{
  "version": 1,
  "generated_at": "2026-08-23T12:03:38Z",
  "description": "Personal site and technical writing by Gabriel Koo, a Hong Kong-based security, cloud, DevOps, automation, and AI platform engineer.",
  "content_types": [
    "article",
    "project",
    "talk",
    "anime"
  ],
  "counts": {
    "article": 33,
    "project": 33,
    "talk": 15,
    "anime": 287
  },
  "items": [
    {
      "id": "article:i-used-cloudflares-app-library-to-map-every-ai-services-domains-and-why-networking-still-matters-2and",
      "source_type": "article",
      "title": "Why I Split-Tunnel My VPN for AI Services — and Let Cloudflare's Application Library Pick the Domains",
      "url": "https://gabrielkoo.com/blog/i-used-cloudflares-app-library-to-map-every-ai-services-domains-and-why-networking-still-matters-2and/",
      "canonical_url": "https://dev.to/gabrielkoo/i-used-cloudflares-app-library-to-map-every-ai-services-domains-and-why-networking-still-matters-2and",
      "published_at": "2026-06-19",
      "last_verified_at": "2026-08-23",
      "tags": [
        "networking",
        "cloudflare",
        "vpn",
        "ai"
      ],
      "description": "I maintain a small set of open-source GitHub repos that hold split-tunnel VPN configs for reaching AI...",
      "content": "I maintain a small set of **open-source GitHub repos** that hold split-tunnel VPN configs for reaching AI services — [Tailscale app connectors](https://github.com/gabrielkoo/tailscale-config-for-ai-services), [WireGuard](https://github.com/gabrielkoo/wireguard-configs-for-ai-services), and [OpenVPN](https://github.com/gabrielkoo/openvpn-configs-for-ai-services). The single hardest part of maintaining them isn't the VPN config. It's answering one deceptively boring question:\n\n**Which domains does this AI platform actually use?**\n\nThis post is about a shortcut I lean on — Cloudflare's Application Library, queried over its REST API — and a broader point I keep coming back to: even in an era where AI can scaffold most of your code and config for you, **networking fundamentals are still worth learning**. Not because the AI can't write the config, but because the difference between a clean setup and one that gets your account flagged comes down to a judgment call — *what you route and what you deliberately don't* — and that judgment is exactly the part an AI can't make for you without understanding your intent.\n\n(One caveat before we start: this is for routing traffic you're already entitled to — not for evading restrictions that legitimately apply to you. Full legitimate-use note at the end.)\n\n## The problem: a \"domain list\" is never just one domain\n\nWhen you want to route only one app through a VPN, you need the set of hostnames (or IPs) that app actually talks to. That sounds trivial until you open DevTools on a modern web app and watch it fan out to twenty different domains: the main API, an auth provider, a CDN for static assets, telemetry, a fraud-detection pixel, a feature-flag service.\n\nRoute too little and the app half-works (login spins forever, streaming responses stall). Route too much — say, the bare `*.cloudflarestorage.com` wildcard — and you scoop up huge amounts of *unrelated* traffic, which defeats the entire point of a split tunnel and can even get your exit node rate-limited.\n\nSo you need the **narrowest accurate** set of hostnames. Where do you get it?\n\n![One app fans out to many hostnames — a single ChatGPT app talking to api.openai.com, auth0.openai.com, cdn.oaistatic.com, oaiusercontent.com and more](/assets/img/caaaed39c4d2.jpg)\n\n## The shortcut: Cloudflare's Application Library\n\nCloudflare's Zero Trust product ships an [**Application Library**](https://developers.cloudflare.com/cloudflare-one/team-and-resources/app-library) — a catalog of well-known SaaS apps, each annotated with the hostnames Cloudflare has observed it using. It's meant for admins building Access policies, but it doubles as a fantastic *domain-discovery* tool. Someone at Cloudflare already did the tedious traffic-watching for ChatGPT, Claude, and friends.\n\n![Stop sniffing traffic by hand — the Cloudflare Application Library hands you the hostnames so you don't have to map them manually](/assets/img/503230b95394.jpg)\n\nYou can browse it in the dashboard under **Zero Trust → Team & Resources → Application Library**. But I don't want to click through a UI every time a provider quietly adds a domain — I want it in code. So I query the [REST API](https://developers.cloudflare.com/api/).\n\nHere's the helper I use to pull ChatGPT's hostnames. Pure stdlib, no SDK:\n\n```python\nimport json, urllib.parse, urllib.request\n\nCF_API = \"https://api.cloudflare.com/client/v4\"\n\n\ndef cf_hostnames(token, account, app_name, search):\n    url = \"%s/accounts/%s/resource-library/applications?%s\" % (\n        CF_API, account, urllib.parse.urlencode({\"search\": search, \"limit\": 25}))\n    req = urllib.request.Request(url, headers={\"Authorization\": \"Bearer \" + token})\n    with urllib.request.urlopen(req, timeout=30) as r:\n        d = json.load(r)\n    for a in (d.get(\"result\") or []):\n        if a.get(\"name\") == app_name:\n            return sorted(set(a.get(\"hostnames\") or []))\n    raise SystemExit(\"CF: application %r not found (search=%r)\" % (app_name, search))\n```\n\nA few things worth calling out:\n\n- **The endpoint is `accounts/{account_id}/resource-library/applications`.** It takes a `search` query and returns matching catalog entries, each with a `hostnames` array. I match on the exact `name` (e.g. `\"ChatGPT\"`, `\"Claude\"`) because a search can return several near-matches.\n- **The token only needs read access** to the resource library. Scope it minimally — there's no reason this token should be able to change anything.\n- **It's deterministic and CI-friendly.** No browser automation, no scraping. That matters for the next step.\n- **You don't need a paid plan.** This lives in Cloudflare's Zero Trust product, and Zero Trust has a [**free tier (up to 50 seats)**](https://blog.cloudflare.com/teams-plans/) that's plenty for personal use. The Application Library and its REST API are available on that free plan — so the entire domain-discovery pipeline costs you nothing but an API token.\n\n### What the API actually returns\n\nHere's the real output for **Claude** (`name == \"Claude\"`) as of this writing:\n\n```json\n{\n  \"service\": \"claude\",\n  \"application\": \"Claude\",\n  \"hostnames\": [\n    \"a-api.anthropic.com\",\n    \"a-cdn.anthropic.com\",\n    \"anthropic.com\",\n    \"claude.ai\",\n    \"claude.com\"\n  ]\n}\n```\n\nFive hostnames, and notice they cover the things that actually matter: the web app (`claude.ai`, `claude.com`), the API (`a-api.anthropic.com`), the static asset CDN (`a-cdn.anthropic.com`), and the marketing/auth origin (`anthropic.com`). That's the *narrow accurate set* I was after — nothing extraneous to prune.\n\nAnd the same call for **ChatGPT** (App ID `1199`):\n\n```json\n{\n  \"service\": \"openai\",\n  \"application\": \"ChatGPT\",\n  \"hostnames\": [\n    \"api.openai.com\",\n    \"auth.openai.com\",\n    \"auth0.openai.com\",\n    \"cdn.oaistatic.com\",\n    \"chat.openai.com\",\n    \"chatgpt.com\",\n    \"oaistatic.com\",\n    \"oaiusercontent.com\",\n    \"openai.com\"\n  ]\n}\n```\n\nNine hostnames, and the list is more revealing than it looks. Miss the Auth0-backed `auth0.openai.com` (still in the login path at time of writing) and sign-in silently hangs; miss `oaiusercontent.com` and uploads/generated files break. Try assembling that from DevTools by hand and you'll miss the auth host you only hit on first login — then spend an afternoon debugging a broken login flow.\n\nCloudflare already did the traffic-watching.\n\n## From hostnames to a routable config\n\nHostnames are perfect for **[Tailscale app connectors](https://tailscale.com/kb/1281/app-connectors)**, which match on the domain name directly — Tailscale handles the DNS-to-IP mapping for you, so the config keeps working even when the provider rotates IPs.\n\nFor **WireGuard and OpenVPN** — which you'd reach for when you need router-level enforcement, or you're on a client where Tailscale isn't an option — you can't route by hostname; those tunnels route by IP. So the pipeline becomes:\n\n1. **Pull hostnames** from the Cloudflare Application Library (the function above).\n2. **Resolve each to A + AAAA records** over DNS-over-HTTPS, so the result doesn't depend on whatever resolver the CI runner happens to use.\n3. **Aggregate** the addresses to CIDR blocks, then collapse overlapping ranges. I default to `/24` (v4) and `/48` (v6), but understand this is a *heuristic*, not precision: expanding one resolved IP to a `/24` also routes the other 255 addresses on that edge node, some of which belong to unrelated tenants. It's a deliberate trade — narrower (`/32`, `/28`) is more precise but churns more often; wider (`/20`, a supernet) means fewer entries but more collateral traffic. Pick the prefix that matches how much drift and collateral you can tolerate.\n4. **Union with a static \"floor\"** for providers that publish a stable, authoritative prefix — for Anthropic that's `160.79.104.0/21` (their own AS399230), so a single resolved `/24` never accidentally blackholes the rest of the block.\n5. **Rewrite the config files** between marker comments, and let a [scheduled GitHub Action](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/.github/workflows/update-ips.yml) open the change.\n\nThe whole thing is one stdlib Python script ([`scripts/update_ips.py`](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/scripts/update_ips.py)) that runs on a cron schedule. The configs stay fresh without me touching them.\n\n![The resolve-and-aggregate pipeline — scattered cloud hostnames funnel through resolve + aggregate into a single clean config](/assets/img/de6fa0294637.jpg)\n\nAnd here's where the two providers diverge in a way that proves the whole point. Run the resolve-and-aggregate step and **ChatGPT** collapses to a pile of Cloudflare ranges:\n\n```text\n# IPv4 (all shared Cloudflare anycast)\n104.18.32.0/23\n104.18.37.0/24\n104.18.41.0/24\n162.159.140.0/24\n172.64.146.0/24\n172.64.150.0/24\n172.64.154.0/23\n172.65.90.0/24\n172.66.0.0/24\n\n# IPv6 — 9 more Cloudflare /48s\n```\n\nEvery one of those is **shared Cloudflare anycast** — [`104.18.x`, `172.64.x`, `172.66.x` are Cloudflare's](https://www.cloudflare.com/ips/), not OpenAI's. Route them and you're routing a slice of Cloudflare's entire customer base, and the specific `/24`s will drift as Cloudflare reshuffles. That's why ChatGPT-by-IP needs the scheduled refresh, and why it's genuinely better as a Tailscale *hostname* connector.\n\n**Claude** collapses to something completely different:\n\n```text\n34.36.57.0/24    ← Google Cloud LB (shared)\n160.79.104.0/21  ← Anthropic's OWN block (AS399230)\n2607:6bc0::/32   ← Anthropic's own IPv6\n```\n\nThe `160.79.104.0/21` is Anthropic's own registered allocation — a `whois`/RDAP lookup shows the block (160.79.104.0–160.79.111.255) registered directly to Anthropic, PBC, announced via AS399230. So I can route the whole `/21` and trust *who* it belongs to, even though it's 2,048 addresses — wider than Claude strictly needs today. That's the deliberate stability-over-precision trade for a block whose owner won't suddenly hand it to someone else. The `34.36.57.0/24` is a [Google Cloud front-end](https://www.gstatic.com/ipranges/cloud.json) I let the script re-resolve each run rather than hardcode, because it *is* shared infrastructure that drifts. **Same pipeline, two completely different risk profiles — and you only know which is which if you understand who owns the address space.**\n\n![Same pipeline, two risk profiles — one resolve+aggregate funnel splits into ChatGPT on shared Cloudflare anycast that drifts, versus Claude on Anthropic's own stable 160.79.104.0/21 block](/assets/img/25e7faa7e55b.jpg)\n\n## Why this is a networking lesson, not a VPN lesson\n\nEvery step above is a networking decision, and getting them wrong has real consequences:\n\n![Hostname routing vs IP routing — hostname routing follows the name and survives IP churn; IP routing pins addresses and breaks on drift](/assets/img/be11d59d9672.jpg)\n\n- **Hostname routing vs. IP routing is a fundamental tradeoff.** DNS/SNI-based routing (Tailscale) survives IP churn and cleanly isolates one app on a shared CDN. IP-CIDR routing (WireGuard/OpenVPN) is simpler and dependency-free but brittle — it breaks when addresses drift, and it *can't* separate two services that share an anycast front end. Knowing which tool fits which provider saves hours.\n- **Shared anycast CDNs are a trap.** `chatgpt.com` sits behind Cloudflare's shared anycast — the same `/24`s serve thousands of unrelated sites. Route the whole block and you've quietly tunneled a chunk of the internet. Route too narrowly and it breaks tomorrow. You have to *understand* that the IP doesn't belong to OpenAI to make a sane call.\n\n![Route a shared /24, scoop unrelated traffic — a net catches the one IP you wanted along with 254 strangers sharing the block](/assets/img/6a15b83413ab.jpg)\n\n- **Knowing who owns an IP block matters.** `160.79.104.0/21` is Anthropic's own allocation (AS399230). That's a stable, safe thing to route wholesale. A `34.36.x` Google Cloud load-balancer front-end in front of the same service is *not* — it's shared infrastructure. A quick `whois` or a look at the AS tells you which is which.\n- **Split tunneling is precision, and precision is how you stay out of trouble.** This circles back to the disclaimer. The reason I route the *narrowest* set of hostnames isn't tidiness — it's that the less unrelated traffic I push through an exit node, the less I look like abuse, and the less I disrupt the security-sensitive services (banking, corporate SSO) that legitimately *don't* want to see me arriving from a VPN IP. Full-tunneling everything is both lazier and riskier.\n\nNone of this requires Cisco's CCNA certification. But it does require knowing what a routing table is, what a CIDR block means, what an autonomous system is, and why DNS resolution is a separate concern from packet routing. That foundation turns \"I copied a VPN config off the internet and hope it works\" into \"I know exactly what traffic goes where, and why.\"\n\n## Takeaways\n\n- **Cloudflare's Application Library is an underrated domain-discovery tool** — queryable over a simple REST endpoint, no scraping required.\n- **Pick your routing primitive to match the provider:** hostname-based for shared CDNs and IP-churning services, IP-CIDR for providers on their own stable address space.\n- **Scope narrowly on purpose.** It's better for performance, it keeps your other services unaffected, and it keeps you on the right side of a provider's abuse heuristics.\n- **And stay legitimate.** Use this for what you're already entitled to use — careful routing, continuity while traveling — not for getting around restrictions that apply to you.\n\nIf you want the runnable code, the three repos are linked at the top. The Cloudflare helper and the full resolve-and-aggregate pipeline live in [`scripts/update_ips.py`](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/scripts/update_ips.py).\n\n## One last thing: the legitimate-use note\n\nI parked this for the end so it didn't bog down the technical walkthrough, but it matters. This whole approach is for one specific, legitimate situation: **your account and home region are permitted to use the service, and you want precise control over your own traffic.** Two concrete cases:\n\n- **You want to route carefully — or deliberately NOT route — to avoid tripping a provider's fraud/abuse checks.** Many platforms run security engines (WAFs, fraud-detection, bot management) that get nervous when a logged-in session suddenly appears from a datacenter IP or a region that doesn't match your account history. This cuts both ways: the same is true of *other* services you use — banks especially — which actively distrust traffic arriving from a VPN exit IP. Precision routing is as much about *keeping the wrong traffic off the tunnel* as putting the right traffic on it.\n- **You normally reside in a region that's allowed to use the service, but you're temporarily somewhere it isn't reachable** — a short work trip, a conference, a layover. You're a legitimate user of an unrestricted region who wants continuity of access you're already entitled to.\n\nWhat this is **not** for: evading a restriction that legitimately applies to you. If your account or country of residence isn't permitted to use a service, a VPN config doesn't change that, and nothing here is an invitation to break a provider's Terms of Service or your local law. **Read the ToS, respect it, and when in doubt don't.** I keep my configs scoped as narrowly as possible precisely *because* the goal is to not look like abuse traffic.\n\n## Sources\n\n- [Cloudflare — Application Library (Cloudflare One docs)](https://developers.cloudflare.com/cloudflare-one/team-and-resources/app-library)\n- [Cloudflare — REST API reference](https://developers.cloudflare.com/api/)\n- [Tailscale — App connectors](https://tailscale.com/kb/1281/app-connectors)\n- [Cloudflare — published IP ranges](https://www.cloudflare.com/ips/) · [Google Cloud — published IP ranges](https://www.gstatic.com/ipranges/cloud.json) · [ARIN RDAP — `160.79.104.0/21` (Anthropic, PBC)](https://rdap.arin.net/registry/ip/160.79.104.0)\n- Companion repos: [Tailscale](https://github.com/gabrielkoo/tailscale-config-for-ai-services) · [WireGuard](https://github.com/gabrielkoo/wireguard-configs-for-ai-services) · [OpenVPN](https://github.com/gabrielkoo/openvpn-configs-for-ai-services)",
      "excerpts": [
        "I maintain a small set of **open-source GitHub repos** that hold split-tunnel VPN configs for reaching AI services — [Tailscale app connectors](https://github.com/gabrielkoo/tailscale-config-for-ai-services), [WireGuard](https://github.com/gabrielkoo/wireguard-configs-for-ai-services), and [OpenVPN](https://github.com/gabrielkoo/openvpn-configs-for-ai-services). The single hardest part of maintaining them isn't the VPN config. It's answering one deceptively boring question:",
        "**Which domains does this AI platform actually use?**",
        "This post is about a shortcut I lean on — Cloudflare's Application Library, queried over its REST API — and a broader point I keep coming back to: even in an era where AI can scaffold most of your code and config for you, **networking fundamentals are still worth learning**. Not because the AI can't write the config, but because the difference between a clean setup and one that gets your account flagged comes down to a judgment call — *what you route and what you deliberately don't* — and that judgment is exactly the part an AI can't make for you without understanding your intent.",
        "(One caveat before we start: this is for routing traffic you're already entitled to — not for evading restrictions that legitimately apply to you. Full legitimate-use note at the end.)",
        "The problem: a \"domain list\" is never just one domain",
        "When you want to route only one app through a VPN, you need the set of hostnames (or IPs) that app actually talks to. That sounds trivial until you open DevTools on a modern web app and watch it fan out to twenty different domains: the main API, an auth provider, a CDN for static assets, telemetry, a fraud-detection pixel, a feature-flag service.",
        "Route too little and the app half-works (login spins forever, streaming responses stall). Route too much — say, the bare `*.cloudflarestorage.com` wildcard — and you scoop up huge amounts of *unrelated* traffic, which defeats the entire point of a split tunnel and can even get your exit node rate-limited.",
        "So you need the **narrowest accurate** set of hostnames. Where do you get it?",
        "![One app fans out to many hostnames — a single ChatGPT app talking to api.openai.com, auth0.openai.com, cdn.oaistatic.com, oaiusercontent.com and more](/assets/img/caaaed39c4d2.jpg)",
        "The shortcut: Cloudflare's Application Library",
        "Cloudflare's Zero Trust product ships an [**Application Library**](https://developers.cloudflare.com/cloudflare-one/team-and-resources/app-library) — a catalog of well-known SaaS apps, each annotated with the hostnames Cloudflare has observed it using. It's meant for admins building Access policies, but it doubles as a fantastic *domain-discovery* tool. Someone at Cloudflare already did the tedious traffic-watching for ChatGPT, Claude, and friends.",
        "![Stop sniffing traffic by hand — the Cloudflare Application Library hands you the hostnames so you don't have to map them manually](/assets/img/503230b95394.jpg)",
        "You can browse it in the dashboard under **Zero Trust → Team & Resources → Application Library**. But I don't want to click through a UI every time a provider quietly adds a domain — I want it in code. So I query the [REST API](https://developers.cloudflare.com/api/).",
        "Here's the helper I use to pull ChatGPT's hostnames. Pure stdlib, no SDK:",
        "CF_API = \"https://api.cloudflare.com/client/v4\"",
        "def cf_hostnames(token, account, app_name, search): url = \"%s/accounts/%s/resource-library/applications?%s\" % ( CF_API, account, urllib.parse.urlencode({\"search\": search, \"limit\": 25})) req = urllib.request.Request(url, headers={\"Authorization\": \"Bearer \" + token}) with urllib.request.urlopen(req, timeout=30) as r: d = json.load(r) for a in (d.get(\"result\") or []): if a.get(\"name\") == app_name: return sorted(set(a.get(\"hostnames\") or [])) raise SystemExit(\"CF: application %r not found (search=%r)\" % (app_name, search)) ```",
        "A few things worth calling out:",
        "- **The endpoint is `accounts/{account_id}/resource-library/applications`.** It takes a `search` query and returns matching catalog entries, each with a `hostnames` array. I match on the exact `name` (e.g. `\"ChatGPT\"`, `\"Claude\"`) because a search can return several near-matches. - **The token only needs read access** to the resource library. Scope it minimally — there's no reason this token should be able to change anything. - **It's deterministic and CI-friendly.** No browser automation, no scraping. That matters for the next step. - **You don't need a paid plan.** This lives in Cloudflare's Zero Trust product, and Zero Trust has a [**free tier (up to 50 seats)**](https://blog.cloudflare.com/teams-plans/) that's plenty for personal use. The Application Library and its REST API are available on that free plan — so the entire domain-discovery pipeline costs you nothing but an API token.",
        "Here's the real output for **Claude** (`name == \"Claude\"`) as of this writing:",
        "Five hostnames, and notice they cover the things that actually matter: the web app (`claude.ai`, `claude.com`), the API (`a-api.anthropic.com`), the static asset CDN (`a-cdn.anthropic.com`), and the marketing/auth origin (`anthropic.com`). That's the *narrow accurate set* I was after — nothing extraneous to prune.",
        "And the same call for **ChatGPT** (App ID `1199`):",
        "Nine hostnames, and the list is more revealing than it looks. Miss the Auth0-backed `auth0.openai.com` (still in the login path at time of writing) and sign-in silently hangs; miss `oaiusercontent.com` and uploads/generated files break. Try assembling that from DevTools by hand and you'll miss the auth host you only hit on first login — then spend an afternoon debugging a broken login flow.",
        "Cloudflare already did the traffic-watching.",
        "From hostnames to a routable config",
        "Hostnames are perfect for **[Tailscale app connectors](https://tailscale.com/kb/1281/app-connectors)**, which match on the domain name directly — Tailscale handles the DNS-to-IP mapping for you, so the config keeps working even when the provider rotates IPs.",
        "For **WireGuard and OpenVPN** — which you'd reach for when you need router-level enforcement, or you're on a client where Tailscale isn't an option — you can't route by hostname; those tunnels route by IP. So the pipeline becomes:",
        "1. **Pull hostnames** from the Cloudflare Application Library (the function above). 2. **Resolve each to A + AAAA records** over DNS-over-HTTPS, so the result doesn't depend on whatever resolver the CI runner happens to use. 3. **Aggregate** the addresses to CIDR blocks, then collapse overlapping ranges. I default to `/24` (v4) and `/48` (v6), but understand this is a *heuristic*, not precision: expanding one resolved IP to a `/24` also routes the other 255 addresses on that edge node, some of which belong to unrelated tenants. It's a deliberate trade — narrower (`/32`, `/28`) is more precise but churns more often; wider (`/20`, a supernet) means fewer entries but more collateral traffic. Pick the prefix that matches how much drift and collateral you can tolerate. 4. **Union with a static \"floor\"** for providers that publish a stable, authoritative prefix — for Anthropic that's `160.79.104.0/21` (their own AS399230), so a single resolved `/24` never accidentally blackholes the rest of the block. 5. **Rewrite the config files** between marker comments, and let a [scheduled GitHub Action](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/.github/workflows/upda",
        "The whole thing is one stdlib Python script ([`scripts/update_ips.py`](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/scripts/update_ips.py)) that runs on a cron schedule. The configs stay fresh without me touching them.",
        "![The resolve-and-aggregate pipeline — scattered cloud hostnames funnel through resolve + aggregate into a single clean config](/assets/img/de6fa0294637.jpg)",
        "And here's where the two providers diverge in a way that proves the whole point. Run the resolve-and-aggregate step and **ChatGPT** collapses to a pile of Cloudflare ranges:",
        "IPv6 — 9 more Cloudflare /48s ```",
        "Every one of those is **shared Cloudflare anycast** — [`104.18.x`, `172.64.x`, `172.66.x` are Cloudflare's](https://www.cloudflare.com/ips/), not OpenAI's. Route them and you're routing a slice of Cloudflare's entire customer base, and the specific `/24`s will drift as Cloudflare reshuffles. That's why ChatGPT-by-IP needs the scheduled refresh, and why it's genuinely better as a Tailscale *hostname* connector.",
        "**Claude** collapses to something completely different:",
        "The `160.79.104.0/21` is Anthropic's own registered allocation — a `whois`/RDAP lookup shows the block (160.79.104.0–160.79.111.255) registered directly to Anthropic, PBC, announced via AS399230. So I can route the whole `/21` and trust *who* it belongs to, even though it's 2,048 addresses — wider than Claude strictly needs today. That's the deliberate stability-over-precision trade for a block whose owner won't suddenly hand it to someone else. The `34.36.57.0/24` is a [Google Cloud front-end](https://www.gstatic.com/ipranges/cloud.json) I let the script re-resolve each run rather than hardcode, because it *is* shared infrastructure that drifts. **Same pipeline, two completely different risk profiles — and you only know which is which if you understand who owns the address space.**",
        "![Same pipeline, two risk profiles — one resolve+aggregate funnel splits into ChatGPT on shared Cloudflare anycast that drifts, versus Claude on Anthropic's own stable 160.79.104.0/21 block](/assets/img/25e7faa7e55b.jpg)",
        "Why this is a networking lesson, not a VPN lesson",
        "Every step above is a networking decision, and getting them wrong has real consequences:",
        "![Hostname routing vs IP routing — hostname routing follows the name and survives IP churn; IP routing pins addresses and breaks on drift](/assets/img/be11d59d9672.jpg)",
        "- **Hostname routing vs. IP routing is a fundamental tradeoff.** DNS/SNI-based routing (Tailscale) survives IP churn and cleanly isolates one app on a shared CDN. IP-CIDR routing (WireGuard/OpenVPN) is simpler and dependency-free but brittle — it breaks when addresses drift, and it *can't* separate two services that share an anycast front end. Knowing which tool fits which provider saves hours. - **Shared anycast CDNs are a trap.** `chatgpt.com` sits behind Cloudflare's shared anycast — the same `/24`s serve thousands of unrelated sites. Route the whole block and you've quietly tunneled a chunk of the internet. Route too narrowly and it breaks tomorrow. You have to *understand* that the IP doesn't belong to OpenAI to make a sane call.",
        "![Route a shared /24, scoop unrelated traffic — a net catches the one IP you wanted along with 254 strangers sharing the block](/assets/img/6a15b83413ab.jpg)",
        "- **Knowing who owns an IP block matters.** `160.79.104.0/21` is Anthropic's own allocation (AS399230). That's a stable, safe thing to route wholesale. A `34.36.x` Google Cloud load-balancer front-end in front of the same service is *not* — it's shared infrastructure. A quick `whois` or a look at the AS tells you which is which. - **Split tunneling is precision, and precision is how you stay out of trouble.** This circles back to the disclaimer. The reason I route the *narrowest* set of hostnames isn't tidiness — it's that the less unrelated traffic I push through an exit node, the less I look like abuse, and the less I disrupt the security-sensitive services (banking, corporate SSO) that legitimately *don't* want to see me arriving from a VPN IP. Full-tunneling everything is both lazier and riskier.",
        "None of this requires Cisco's CCNA certification. But it does require knowing what a routing table is, what a CIDR block means, what an autonomous system is, and why DNS resolution is a separate concern from packet routing. That foundation turns \"I copied a VPN config off the internet and hope it works\" into \"I know exactly what traffic goes where, and why.\"",
        "- **Cloudflare's Application Library is an underrated domain-discovery tool** — queryable over a simple REST endpoint, no scraping required. - **Pick your routing primitive to match the provider:** hostname-based for shared CDNs and IP-churning services, IP-CIDR for providers on their own stable address space. - **Scope narrowly on purpose.** It's better for performance, it keeps your other services unaffected, and it keeps you on the right side of a provider's abuse heuristics. - **And stay legitimate.** Use this for what you're already entitled to use — careful routing, continuity while traveling — not for getting around restrictions that apply to you.",
        "If you want the runnable code, the three repos are linked at the top. The Cloudflare helper and the full resolve-and-aggregate pipeline live in [`scripts/update_ips.py`](https://github.com/gabrielkoo/wireguard-configs-for-ai-services/blob/main/scripts/update_ips.py).",
        "One last thing: the legitimate-use note",
        "I parked this for the end so it didn't bog down the technical walkthrough, but it matters. This whole approach is for one specific, legitimate situation: **your account and home region are permitted to use the service, and you want precise control over your own traffic.** Two concrete cases:",
        "- **You want to route carefully — or deliberately NOT route — to avoid tripping a provider's fraud/abuse checks.** Many platforms run security engines (WAFs, fraud-detection, bot management) that get nervous when a logged-in session suddenly appears from a datacenter IP or a region that doesn't match your account history. This cuts both ways: the same is true of *other* services you use — banks especially — which actively distrust traffic arriving from a VPN exit IP. Precision routing is as much about *keeping the wrong traffic off the tunnel* as putting the right traffic on it. - **You normally reside in a region that's allowed to use the service, but you're temporarily somewhere it isn't reachable** — a short work trip, a conference, a layover. You're a legitimate user of an unrestricted region who wants continuity of access you're already entitled to.",
        "What this is **not** for: evading a restriction that legitimately applies to you. If your account or country of residence isn't permitted to use a service, a VPN config doesn't change that, and nothing here is an invitation to break a provider's Terms of Service or your local law. **Read the ToS, respect it, and when in doubt don't.** I keep my configs scoped as narrowly as possible precisely *because* the goal is to not look like abuse traffic.",
        "- [Cloudflare — Application Library (Cloudflare One docs)](https://developers.cloudflare.com/cloudflare-one/team-and-resources/app-library) - [Cloudflare — REST API reference](https://developers.cloudflare.com/api/) - [Tailscale — App connectors](https://tailscale.com/kb/1281/app-connectors) - [Cloudflare — published IP ranges](https://www.cloudflare.com/ips/) · [Google Cloud — published IP ranges](https://www.gstatic.com/ipranges/cloud.json) · [ARIN RDAP — `160.79.104.0/21` (Anthropic, PBC)](https://rdap.arin.net/registry/ip/160.79.104.0) - Companion repos: [Tailscale](https://github.com/gabrielkoo/tailscale-config-for-ai-services) · [WireGuard](https://github.com/gabrielkoo/wireguard-configs-for-ai-services) · [OpenVPN](https://github.com/gabrielkoo/openvpn-configs-for-ai-services)"
      ]
    },
    {
      "id": "article:stop-putting-api-keys-in-mcpjson-per-user-oauth-with-amazon-cognito-aws-lambda-4h2i",
      "source_type": "article",
      "title": "Stop Putting API Keys in Your MCP Config: Per-User OAuth with Amazon Cognito + AWS Lambda",
      "url": "https://gabrielkoo.com/blog/stop-putting-api-keys-in-mcpjson-per-user-oauth-with-amazon-cognito-aws-lambda-4h2i/",
      "canonical_url": "https://dev.to/aws-builders/stop-putting-api-keys-in-mcpjson-per-user-oauth-with-amazon-cognito-aws-lambda-4h2i",
      "published_at": "2026-06-07",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "security",
        "ai",
        "mcp"
      ],
      "description": "The runnable companion to my AgentCon HK 2026 talk, \"Empower Team-Wide Vibe Coding with LLM Gateway...",
      "content": "_The runnable companion to my AgentCon HK 2026 talk, [\"Empower Team-Wide Vibe Coding with LLM Gateway and Security-First MCPs.\"](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/) The talk argued per-user OAuth is what turns a shared god-token into safe, auditable agent access. This is the wiring — explained at the architecture level. The full `template.yaml`, Lambda, and scripts live in the public repo linked at the end._\n\n---\n\n## The gap nobody fills: *your* identity in front of a shared-key API\n\nBy 2026 most big SaaS shipped an official MCP server, and many even support OAuth. So \"my tool has no MCP\" and \"my tool has no OAuth\" are both fading problems — chasing vendors that lack one is a losing game; the list shrinks every week.\n\nTavily — the search API I use here — is actually a good citizen: its remote MCP supports both a shared key in the URL (`https://mcp.tavily.com/mcp/?tavilyApiKey=<KEY>`) *and* an OAuth flow. So why wrap it at all? Because even a vendor's OAuth answers a different question than the one a security team is asking:\n\n- **Shared keys are the default, and cost is why.** Plenty of these tools price per seat. So the cheapest integration — the one teams actually ship — is one account's API key wired into a single shared MCP for the whole group. The instant that key is shared, per-user identity is gone: every call is the same caller, and \"who ran this?\" has no answer. Vendor OAuth only helps if everyone pays for their own seat, which is exactly the bill teams are dodging.\n- **Claimed ≠ enforced.** Even when a tool *does* attribute per user, it often rides on a value the client supplies — Tavily's `X-Human-Id` header is exactly this. The client asserts it, so nothing stops a caller from sending someone else's; the attribution is a courtesy, not a control. Trustworthy attribution has to come from a token *your own* gateway cryptographically verified, not a string the client typed.\n\nAnd here's the part that doesn't shrink at all: **the upstreams that matter most to you will never ship OAuth.** Your internal billing API, the legacy claims system nobody wants to touch, the compliance-mandated third-party tool with a single team API key. For every one of those, the only auth is a shared key — shared by nature. And it's not only internal systems: plenty of public vendors are the same shape — Brave Search's own official MCP server, for instance, authenticates with a single `BRAVE_API_KEY` and no OAuth at all. One key, the whole team behind it.\n\nThat's the real, durable gap: **no front door that authenticates callers as *your* identity, scopes them, and audits them — before handing off to a shared-key upstream.** That's what this post builds. Tavily is just a free, public stand-in you can actually run; mentally swap it for your own shared-key API.\n\n> 💡 **The one idea:** put a real OAuth 2.0 / OIDC authorization server bound to *your* identity (Amazon Cognito) — running the PKCE and client-credentials flows the MCP spec expects — in front of a shared-key upstream, in a single Lambda. Each caller gets their own scoped, cryptographically-verified, audited identity. The shared key never leaves the server.\n\n*Want the 30-second version first? [Walk through the interactive demo](https://gabrielkoo.github.io/tavily-oauth-mcp-wrapper/) — click through the whole OAuth flow, the scoped tool call, and the `403` when a caller reaches past its scope.*\n\n## The architecture\n\nTwo pictures tell the whole story. The shared-key path — one key for everyone, whether it sits in a URL or a config file:\n\n![Shared-key access — three callers (two engineers and an agent) all funnel through one shared key, which then hits the upstream API. No per-user scope under your control, no audit you own, the key leaks into logs, and rotating it breaks the whole team.](/assets/img/3c5b6ff475fb.png)\n\nWith the wrapper, each caller arrives as themselves — verified against your own identity provider — and the key is locked away server-side:\n\n![The OAuth wrapper — each caller logs in to its own session, hits an MCP server fronted by Amazon Cognito OAuth that enforces per-user scope and audit, and the shared key sits server-side (Secrets Manager / Parameter Store), reached only by the server before it calls the upstream. Scoped per identity, fully audited, key never leaves the server, and one session can be revoked without touching the team.](/assets/img/14d02ac3c62f.png)\n\nThree managed AWS pieces do the work, each with one clear job:\n\n**Amazon Cognito — the bouncer who issues the wristbands.** It's the OAuth 2.0 / OIDC authorization server (with PKCE — the piece OAuth 2.1 and the MCP spec lean on). A caller proves who they are *to Amazon Cognito*, which hands back a short-lived signed token stamped with a scope (here, `tavily-mcp/search`). It handles both kinds of caller from the same pool: a backend agent authenticates machine-to-machine, a human logs in through a hosted login page with PKCE. Crucially, Amazon Cognito *is* your identity layer — swapping the demo's username/password for real corporate SSO (SAML, OIDC, or social logins like Google, Microsoft, Apple) is a configuration change, not a rebuild. The tokens, scopes, and everything downstream stay identical.\n\n**API Gateway HTTP API + its JWT authorizer — the wristband scanner at the door.** Every request to the MCP endpoint — a plain JSON-RPC call over HTTP POST (the Streamable HTTP transport; this wrapper uses request/response POSTs only, not the transport's optional SSE streaming channel) — hits API Gateway first. Its [built-in JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) — a native feature of the HTTP API (v2) flavour; the older REST API would need a Cognito or custom Lambda authorizer instead — checks the token's signature, issuer, and expiry *at the edge*, before a single line of your code runs. No token, expired token, forged token: rejected with a 401 right there. This is pure authentication: *are you who the token says you are?* Nothing reaches your logic until that passes. (One footgun worth flagging: Cognito access tokens carry no `aud` claim, so the authorizer's audience must be set to the Cognito app client ID — a mismatch here is the most common source of silent 401s in this stack.)\n\n**Lambda — the room you're actually allowed into.** Once the token is valid, the Lambda does the *authorization*: it re-reads the scope to confirm this identity may call this specific tool, writes an audit line tying the action to that caller, then — and only then — reaches into Secrets Manager for the shared upstream key and calls Tavily. The key lives only inside this function's execution role; it is never sent back to the client.\n\n![The deployed request path — MCP client sends an Amazon Cognito Bearer token; API Gateway's JWT authorizer validates it at the edge; the Lambda re-checks scope, logs the caller, and calls Tavily with a key it reads from Secrets Manager and never returns to the client.](/assets/img/c700c0918921.png)\n\nThe split is the whole point: **API Gateway answers \"is this a real, valid token?\" and Lambda answers \"is this identity allowed to do this, and let's record that they did.\"** Authentication at the edge, authorization in your code, the secret sealed behind both.\n\n## Why `type: http`, not `type: stdio`\n\nThere's a reason this wrapper is a *remote* MCP server and not a local one — and it's the same reason the talk put OAuth-fronted remote servers at the centre of the architecture. An MCP client config gives you three transport choices, and the choice quietly decides your entire security story:\n\n- **`stdio`** — the client spawns a local process (`npx some-mcp`, a Python script) and talks to it over stdin/stdout. The catch: that process needs the upstream credential *on the developer's machine*. So the key lands in `mcp.json`, in an `env` block, in shell history, in a dotfile that gets synced to who-knows-where. Every laptop is now a copy of the shared key. This is the exact shape of the BYOAI / shared-key problem — one credential, sprayed everywhere, impossible to attribute or revoke cleanly.\n- **`sse`** — the older remote transport. Remote is the right instinct, but plain SSE was only ever a transport; it never carried an auth story of its own, so in practice people bolted a static bearer token onto it and landed right back at \"one shared secret.\"\n- **`http`** (Streamable HTTP) — a remote endpoint the client reaches over plain HTTPS, and the transport the 2025 MCP spec standardised on. Crucially, the [spec defines the MCP server as an **OAuth resource server**](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) — so authentication is a first-class part of the transport, not an afterthought. The client config holds *no secret at all*; it just points at a URL and lets OAuth do the rest. (With an OAuth-native client the browser login is automatic; clients that don't yet drive the flow themselves lean on a small local helper to do it — more on that below.)\n\nThat last line is the whole pitch. Compare the two configs a developer actually writes:\n\n```jsonc\n// stdio — the secret lives on every laptop\n{ \"tavily\": { \"type\": \"stdio\", \"command\": \"npx\",\n    \"args\": [\"tavily-mcp\"],\n    \"env\": { \"TAVILY_API_KEY\": \"tvly-SHARED-KEY-everyone-has-this\" } } }\n\n// http — no secret, OAuth per user\n{ \"tavily\": { \"type\": \"http\", \"url\": \"https://…/mcp\" } }\n```\n\nThe `http` version has nothing to leak. The credential never reaches the client; identity is established per-user through Cognito; and the server — not the laptop — is the only thing that ever touches the upstream key. Whenever an upstream *can* be reached as a remote OAuth-fronted `http` server, it should be. This wrapper exists precisely to turn a `stdio`-shaped shared-key tool into an `http`-shaped one.\n\n## One endpoint, two kinds of caller\n\nA backend agent and a human engineer authenticate through completely different OAuth flows — `client_credentials` for the machine, `authorization_code` + PKCE for the person. But they end up carrying the *same* scope and hitting the *same* endpoint. The server treats them identically; the only difference is what lands in the audit log — a client ID for the machine, an email for the human.\n\n![The two token origins converge on one MCP endpoint — a machine client via client_credentials and a human via authorization_code + PKCE both arrive carrying the same tavily-mcp/search scope, and the server treats them identically while logging a different caller identity.](/assets/img/f42275ee79db.png)\n\n### The last mile most \"MCP + OAuth\" posts skip\n\nOne piece of the human flow is easy to gloss over, and it's exactly where most \"remote MCP + OAuth\" walkthroughs quietly wave their hands: a static MCP client config (the `claude_desktop_config.json` kind) can't pop a browser for a Cognito login on its own. The fix is a thin local helper — registered as the client's `stdio` command — that checks for a cached token, triggers the PKCE browser login when it's missing or expired, stashes the result, and forwards each request with the `Authorization: Bearer …` header. This is exactly what tools like [`mcp-remote`](https://github.com/geelen/mcp-remote) exist to do — and the [rough edges around that login flow](https://github.com/geelen/mcp-remote/issues/251) are a recurring [source of confusion](https://www.reddit.com/r/mcp/comments/1mw09b5/how_are_you_handling_oauth_when_running_mcp/) when people first wire up a remote MCP server. It's a small shim, but without it the \"human logs in with PKCE\" line is doing a lot of unspoken work. The repo includes a runnable PKCE script you can wire in as that helper.\n\n## The honest limitation\n\n**The Lambda is the *final* enforcement point — but the security of the whole scheme rests on the entire chain: the JWT authorizer validating token integrity at the edge, and the Lambda enforcing scope behind it.** The upstream still sees one shared key; it has no idea which human is behind a call. All the per-user identity, scoping, and audit live in *your* layer. So the scheme reduces to a few disciplines:\n\n- **Lock the secret to only the Lambda's execution role.** Anyone who can read it gets full upstream access.\n- **Make sure every route to the function goes through the JWT authorizer.** A second unprotected trigger would bypass the whole thing.\n- **Machine callers blur the human behind them.** When a backend agent authenticates with `client_credentials`, the audit log names the *machine* client, not the person who prompted it. For a human-triggered agent you've narrowed \"who\" to one service identity, not one person. Closing that last gap means propagating the user's own token down to the agent (OAuth 2.0 token exchange, RFC 8693) instead of falling back to a shared machine credential — worth knowing before you lean on M2M audit lines as proof of *who* did something.\n- **Mind the 30-second ceiling.** API Gateway HTTP APIs cap each integration at 30 seconds. A search call returns in well under a second, so it's a non-issue here — but if you wrap a slower upstream (a heavy query, a multi-step scrape, a long-running agent tool), a call that overruns hits a `504` at the gateway. That's the point where you'd reach for the transport's SSE streaming channel, or an async submit-then-poll pattern, instead of a single blocking POST.\n\nGet those right and you've genuinely converted a shared god-token into per-user delegation. Get them wrong and you've just added a proxy in front of the same shared key. This is also why the *audit log* matters more than it looks: it's the only place \"who ran what\" is recoverable at all — Tavily's logs will forever show one key.\n\n## It works\n\nDeployed to a real AWS account, the end-to-end path is exactly what you'd hope. A caller fetches an Amazon Cognito token, calls the MCP tool, and a real Tavily answer comes back *through the wrapper* — the client never touches the upstream key:\n\n```text\nAnswer: The Model Context Protocol (MCP) is a standardized framework\nenabling AI models to access external data sources and tools securely…\n\n1. What is the Model Context Protocol (MCP)?\n   https://www.databricks.com/blog/what-is-model-context-protocol\n```\n\nAnd the boundary holds: a request with **no token, or a forged one, gets a `401` at the edge** — rejected by API Gateway before the Lambda ever runs. Meanwhile every successful call writes an audit line naming the caller — `client 59psk…` or `demo@example.com` — something the upstream's shared-key logs physically cannot produce. (Commands and full output are in the repo.)\n\n## Why this generalizes\n\nSwap Tavily for a legacy internal API, a SaaS whose own OAuth logs into *its* identity instead of yours, or a machine credential you don't want sprayed across developer laptops — the Lambda is the only thing that changes, and the payoff is the same. A leaked Amazon Cognito access token is short-lived and expires on its own, and you cut a compromised identity off at the source by revoking its refresh token (and disabling the user/app client) so it can't mint new ones — versus a leaked shared key, which means rotate-and-redeploy for the whole team. (Worth knowing: with the edge JWT authorizer, an already-issued access token stays valid until it expires; for instant kill-switch revocation you'd add a server-side check in the Lambda.) It ties straight back to the Golden Rule from the talk — *if a human can't do it in the UI, the agent can't do it via MCP* — because the agent inherits exactly the caller's scope, nothing more. That's what makes team-wide AI-assisted coding safe to roll out: developers plug a powerful shared-key tool straight into their own IDE, and security doesn't have to say no, because there's no shared credential on the laptop to leak — just the developer's own scoped, revocable session.\n\n## What does all this cost? Almost nothing.\n\nHere's the part that makes this an easy yes: the entire control plane is serverless and pay-per-use, so for a team's real workload it rounds to a rounding error. Amazon Cognito is free up to 50,000 monthly active users. Lambda's free tier covers a million requests a month. An idle HTTP API costs nothing and is about $1 per million requests after that.\n\nThe one real decision is where the shared upstream key lives. An encrypted Lambda environment variable is free but readable by anyone with `lambda:GetFunctionConfiguration` and baked into the deployment — fine for a demo, not for production. SSM Parameter Store `SecureString` is also free and gives the same *at-rest* protection for a static key (KMS-encrypted, IAM-scoped reads) — the sweet spot for most single-key wrappers. AWS Secrets Manager costs ~$0.40/month but adds the operational layer you may actually need: automatic rotation, resource policies, cross-account sharing. I used Secrets Manager in the reference because it makes the lockdown story explicit, but Parameter Store swaps in with a few lines. Either way the boundary is identical: IAM grants read to *only* the Lambda's execution role, and the key never leaves the server.\n\nYou also get *measurement* nearly for free: because every call carries a real identity, your Lambda can log structured per-user, per-tool fields, and CloudWatch Logs Insights (or an EMF metric) turns them into per-identity rate limits, anomaly alerts, or usage you bill back to a team. The shared-key world gives you one undifferentiated blob of traffic and none of that.\n\nFull source — `template.yaml`, the Lambda, both flow scripts, and a one-command teardown — is on GitHub. Clone it, point it at your own shared-key API, and you've got per-user OAuth in an afternoon.\n\n---\n\n### Further reading\n\n- **The talk this builds on** — [\"Empower Team-Wide Vibe Coding with LLM Gateway and Security-First MCPs\"](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/) (Gabriel Koo & Rakshit Jain, AgentCon HK 2026).\n- **Interactive demo** — [click through the full OAuth flow](https://gabrielkoo.github.io/tavily-oauth-mcp-wrapper/) (login, scoped call, 403 on out-of-scope tool, and the `stdio` vs `http` config contrast).\n- **Runnable demo & full source**: [github.com/gabrielkoo/tavily-oauth-mcp-wrapper](https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper)",
      "excerpts": [
        "_The runnable companion to my AgentCon HK 2026 talk, [\"Empower Team-Wide Vibe Coding with LLM Gateway and Security-First MCPs.\"](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/) The talk argued per-user OAuth is what turns a shared god-token into safe, auditable agent access. This is the wiring — explained at the architecture level. The full `template.yaml`, Lambda, and scripts live in the public repo linked at the end._",
        "The gap nobody fills: *your* identity in front of a shared-key API",
        "By 2026 most big SaaS shipped an official MCP server, and many even support OAuth. So \"my tool has no MCP\" and \"my tool has no OAuth\" are both fading problems — chasing vendors that lack one is a losing game; the list shrinks every week.",
        "Tavily — the search API I use here — is actually a good citizen: its remote MCP supports both a shared key in the URL (`https://mcp.tavily.com/mcp/?tavilyApiKey= `) *and* an OAuth flow. So why wrap it at all? Because even a vendor's OAuth answers a different question than the one a security team is asking:",
        "- **Shared keys are the default, and cost is why.** Plenty of these tools price per seat. So the cheapest integration — the one teams actually ship — is one account's API key wired into a single shared MCP for the whole group. The instant that key is shared, per-user identity is gone: every call is the same caller, and \"who ran this?\" has no answer. Vendor OAuth only helps if everyone pays for their own seat, which is exactly the bill teams are dodging. - **Claimed ≠ enforced.** Even when a tool *does* attribute per user, it often rides on a value the client supplies — Tavily's `X-Human-Id` header is exactly this. The client asserts it, so nothing stops a caller from sending someone else's; the attribution is a courtesy, not a control. Trustworthy attribution has to come from a token *your own* gateway cryptographically verified, not a string the client typed.",
        "And here's the part that doesn't shrink at all: **the upstreams that matter most to you will never ship OAuth.** Your internal billing API, the legacy claims system nobody wants to touch, the compliance-mandated third-party tool with a single team API key. For every one of those, the only auth is a shared key — shared by nature. And it's not only internal systems: plenty of public vendors are the same shape — Brave Search's own official MCP server, for instance, authenticates with a single `BRAVE_API_KEY` and no OAuth at all. One key, the whole team behind it.",
        "That's the real, durable gap: **no front door that authenticates callers as *your* identity, scopes them, and audits them — before handing off to a shared-key upstream.** That's what this post builds. Tavily is just a free, public stand-in you can actually run; mentally swap it for your own shared-key API.",
        "> 💡 **The one idea:** put a real OAuth 2.0 / OIDC authorization server bound to *your* identity (Amazon Cognito) — running the PKCE and client-credentials flows the MCP spec expects — in front of a shared-key upstream, in a single Lambda. Each caller gets their own scoped, cryptographically-verified, audited identity. The shared key never leaves the server.",
        "*Want the 30-second version first? [Walk through the interactive demo](https://gabrielkoo.github.io/tavily-oauth-mcp-wrapper/) — click through the whole OAuth flow, the scoped tool call, and the `403` when a caller reaches past its scope.*",
        "Two pictures tell the whole story. The shared-key path — one key for everyone, whether it sits in a URL or a config file:",
        "![Shared-key access — three callers (two engineers and an agent) all funnel through one shared key, which then hits the upstream API. No per-user scope under your control, no audit you own, the key leaks into logs, and rotating it breaks the whole team.](/assets/img/3c5b6ff475fb.png)",
        "With the wrapper, each caller arrives as themselves — verified against your own identity provider — and the key is locked away server-side:",
        "![The OAuth wrapper — each caller logs in to its own session, hits an MCP server fronted by Amazon Cognito OAuth that enforces per-user scope and audit, and the shared key sits server-side (Secrets Manager / Parameter Store), reached only by the server before it calls the upstream. Scoped per identity, fully audited, key never leaves the server, and one session can be revoked without touching the team.](/assets/img/14d02ac3c62f.png)",
        "Three managed AWS pieces do the work, each with one clear job:",
        "**Amazon Cognito — the bouncer who issues the wristbands.** It's the OAuth 2.0 / OIDC authorization server (with PKCE — the piece OAuth 2.1 and the MCP spec lean on). A caller proves who they are *to Amazon Cognito*, which hands back a short-lived signed token stamped with a scope (here, `tavily-mcp/search`). It handles both kinds of caller from the same pool: a backend agent authenticates machine-to-machine, a human logs in through a hosted login page with PKCE. Crucially, Amazon Cognito *is* your identity layer — swapping the demo's username/password for real corporate SSO (SAML, OIDC, or social logins like Google, Microsoft, Apple) is a configuration change, not a rebuild. The tokens, scopes, and everything downstream stay identical.",
        "**API Gateway HTTP API + its JWT authorizer — the wristband scanner at the door.** Every request to the MCP endpoint — a plain JSON-RPC call over HTTP POST (the Streamable HTTP transport; this wrapper uses request/response POSTs only, not the transport's optional SSE streaming channel) — hits API Gateway first. Its [built-in JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) — a native feature of the HTTP API (v2) flavour; the older REST API would need a Cognito or custom Lambda authorizer instead — checks the token's signature, issuer, and expiry *at the edge*, before a single line of your code runs. No token, expired token, forged token: rejected with a 401 right there. This is pure authentication: *are you who the token says you are?* Nothing reaches your logic until that passes. (One footgun worth flagging: Cognito access tokens carry no `aud` claim, so the authorizer's audience must be set to the Cognito app client ID — a mismatch here is the most common source of silent 401s in this stack.)",
        "**Lambda — the room you're actually allowed into.** Once the token is valid, the Lambda does the *authorization*: it re-reads the scope to confirm this identity may call this specific tool, writes an audit line tying the action to that caller, then — and only then — reaches into Secrets Manager for the shared upstream key and calls Tavily. The key lives only inside this function's execution role; it is never sent back to the client.",
        "![The deployed request path — MCP client sends an Amazon Cognito Bearer token; API Gateway's JWT authorizer validates it at the edge; the Lambda re-checks scope, logs the caller, and calls Tavily with a key it reads from Secrets Manager and never returns to the client.](/assets/img/c700c0918921.png)",
        "The split is the whole point: **API Gateway answers \"is this a real, valid token?\" and Lambda answers \"is this identity allowed to do this, and let's record that they did.\"** Authentication at the edge, authorization in your code, the secret sealed behind both.",
        "Why `type: http`, not `type: stdio`",
        "There's a reason this wrapper is a *remote* MCP server and not a local one — and it's the same reason the talk put OAuth-fronted remote servers at the centre of the architecture. An MCP client config gives you three transport choices, and the choice quietly decides your entire security story:",
        "- **`stdio`** — the client spawns a local process (`npx some-mcp`, a Python script) and talks to it over stdin/stdout. The catch: that process needs the upstream credential *on the developer's machine*. So the key lands in `mcp.json`, in an `env` block, in shell history, in a dotfile that gets synced to who-knows-where. Every laptop is now a copy of the shared key. This is the exact shape of the BYOAI / shared-key problem — one credential, sprayed everywhere, impossible to attribute or revoke cleanly. - **`sse`** — the older remote transport. Remote is the right instinct, but plain SSE was only ever a transport; it never carried an auth story of its own, so in practice people bolted a static bearer token onto it and landed right back at \"one shared secret.\" - **`http`** (Streamable HTTP) — a remote endpoint the client reaches over plain HTTPS, and the transport the 2025 MCP spec standardised on. Crucially, the [spec defines the MCP server as an **OAuth resource server**](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) — so authentication is a first-class part of the transport, not an afterthought. The client config holds *no secret at all*; it just poi",
        "That last line is the whole pitch. Compare the two configs a developer actually writes:",
        "// http — no secret, OAuth per user { \"tavily\": { \"type\": \"http\", \"url\": \"https://…/mcp\" } } ```",
        "The `http` version has nothing to leak. The credential never reaches the client; identity is established per-user through Cognito; and the server — not the laptop — is the only thing that ever touches the upstream key. Whenever an upstream *can* be reached as a remote OAuth-fronted `http` server, it should be. This wrapper exists precisely to turn a `stdio`-shaped shared-key tool into an `http`-shaped one.",
        "One endpoint, two kinds of caller",
        "A backend agent and a human engineer authenticate through completely different OAuth flows — `client_credentials` for the machine, `authorization_code` + PKCE for the person. But they end up carrying the *same* scope and hitting the *same* endpoint. The server treats them identically; the only difference is what lands in the audit log — a client ID for the machine, an email for the human.",
        "![The two token origins converge on one MCP endpoint — a machine client via client_credentials and a human via authorization_code + PKCE both arrive carrying the same tavily-mcp/search scope, and the server treats them identically while logging a different caller identity.](/assets/img/f42275ee79db.png)",
        "The last mile most \"MCP + OAuth\" posts skip",
        "One piece of the human flow is easy to gloss over, and it's exactly where most \"remote MCP + OAuth\" walkthroughs quietly wave their hands: a static MCP client config (the `claude_desktop_config.json` kind) can't pop a browser for a Cognito login on its own. The fix is a thin local helper — registered as the client's `stdio` command — that checks for a cached token, triggers the PKCE browser login when it's missing or expired, stashes the result, and forwards each request with the `Authorization: Bearer …` header. This is exactly what tools like [`mcp-remote`](https://github.com/geelen/mcp-remote) exist to do — and the [rough edges around that login flow](https://github.com/geelen/mcp-remote/issues/251) are a recurring [source of confusion](https://www.reddit.com/r/mcp/comments/1mw09b5/how_are_you_handling_oauth_when_running_mcp/) when people first wire up a remote MCP server. It's a small shim, but without it the \"human logs in with PKCE\" line is doing a lot of unspoken work. The repo includes a runnable PKCE script you can wire in as that helper.",
        "**The Lambda is the *final* enforcement point — but the security of the whole scheme rests on the entire chain: the JWT authorizer validating token integrity at the edge, and the Lambda enforcing scope behind it.** The upstream still sees one shared key; it has no idea which human is behind a call. All the per-user identity, scoping, and audit live in *your* layer. So the scheme reduces to a few disciplines:",
        "- **Lock the secret to only the Lambda's execution role.** Anyone who can read it gets full upstream access. - **Make sure every route to the function goes through the JWT authorizer.** A second unprotected trigger would bypass the whole thing. - **Machine callers blur the human behind them.** When a backend agent authenticates with `client_credentials`, the audit log names the *machine* client, not the person who prompted it. For a human-triggered agent you've narrowed \"who\" to one service identity, not one person. Closing that last gap means propagating the user's own token down to the agent (OAuth 2.0 token exchange, RFC 8693) instead of falling back to a shared machine credential — worth knowing before you lean on M2M audit lines as proof of *who* did something. - **Mind the 30-second ceiling.** API Gateway HTTP APIs cap each integration at 30 seconds. A search call returns in well under a second, so it's a non-issue here — but if you wrap a slower upstream (a heavy query, a multi-step scrape, a long-running agent tool), a call that overruns hits a `504` at the gateway. That's the point where you'd reach for the transport's SSE streaming channel, or an async submit-then-poll pa",
        "Get those right and you've genuinely converted a shared god-token into per-user delegation. Get them wrong and you've just added a proxy in front of the same shared key. This is also why the *audit log* matters more than it looks: it's the only place \"who ran what\" is recoverable at all — Tavily's logs will forever show one key.",
        "Deployed to a real AWS account, the end-to-end path is exactly what you'd hope. A caller fetches an Amazon Cognito token, calls the MCP tool, and a real Tavily answer comes back *through the wrapper* — the client never touches the upstream key:",
        "1. What is the Model Context Protocol (MCP)? https://www.databricks.com/blog/what-is-model-context-protocol ```",
        "And the boundary holds: a request with **no token, or a forged one, gets a `401` at the edge** — rejected by API Gateway before the Lambda ever runs. Meanwhile every successful call writes an audit line naming the caller — `client 59psk…` or `demo@example.com` — something the upstream's shared-key logs physically cannot produce. (Commands and full output are in the repo.)",
        "Swap Tavily for a legacy internal API, a SaaS whose own OAuth logs into *its* identity instead of yours, or a machine credential you don't want sprayed across developer laptops — the Lambda is the only thing that changes, and the payoff is the same. A leaked Amazon Cognito access token is short-lived and expires on its own, and you cut a compromised identity off at the source by revoking its refresh token (and disabling the user/app client) so it can't mint new ones — versus a leaked shared key, which means rotate-and-redeploy for the whole team. (Worth knowing: with the edge JWT authorizer, an already-issued access token stays valid until it expires; for instant kill-switch revocation you'd add a server-side check in the Lambda.) It ties straight back to the Golden Rule from the talk — *if a human can't do it in the UI, the agent can't do it via MCP* — because the agent inherits exactly the caller's scope, nothing more. That's what makes team-wide AI-assisted coding safe to roll out: developers plug a powerful shared-key tool straight into their own IDE, and security doesn't have to say no, because there's no shared credential on the laptop to leak — just the developer's own scope",
        "What does all this cost? Almost nothing.",
        "Here's the part that makes this an easy yes: the entire control plane is serverless and pay-per-use, so for a team's real workload it rounds to a rounding error. Amazon Cognito is free up to 50,000 monthly active users. Lambda's free tier covers a million requests a month. An idle HTTP API costs nothing and is about $1 per million requests after that.",
        "The one real decision is where the shared upstream key lives. An encrypted Lambda environment variable is free but readable by anyone with `lambda:GetFunctionConfiguration` and baked into the deployment — fine for a demo, not for production. SSM Parameter Store `SecureString` is also free and gives the same *at-rest* protection for a static key (KMS-encrypted, IAM-scoped reads) — the sweet spot for most single-key wrappers. AWS Secrets Manager costs ~$0.40/month but adds the operational layer you may actually need: automatic rotation, resource policies, cross-account sharing. I used Secrets Manager in the reference because it makes the lockdown story explicit, but Parameter Store swaps in with a few lines. Either way the boundary is identical: IAM grants read to *only* the Lambda's execution role, and the key never leaves the server.",
        "You also get *measurement* nearly for free: because every call carries a real identity, your Lambda can log structured per-user, per-tool fields, and CloudWatch Logs Insights (or an EMF metric) turns them into per-identity rate limits, anomaly alerts, or usage you bill back to a team. The shared-key world gives you one undifferentiated blob of traffic and none of that.",
        "Full source — `template.yaml`, the Lambda, both flow scripts, and a one-command teardown — is on GitHub. Clone it, point it at your own shared-key API, and you've got per-user OAuth in an afternoon.",
        "- **The talk this builds on** — [\"Empower Team-Wide Vibe Coding with LLM Gateway and Security-First MCPs\"](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/) (Gabriel Koo & Rakshit Jain, AgentCon HK 2026). - **Interactive demo** — [click through the full OAuth flow](https://gabrielkoo.github.io/tavily-oauth-mcp-wrapper/) (login, scoped call, 403 on out-of-scope tool, and the `stdio` vs `http` config contrast). - **Runnable demo & full source**: [github.com/gabrielkoo/tavily-oauth-mcp-wrapper](https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper)"
      ]
    },
    {
      "id": "article:resurface-claude-code-usage-across-your-team-with-cloudwatch-otel-no-lambda-4p0i",
      "source_type": "article",
      "title": "Resurface Claude Code Usage Across Your Team with CloudWatch OTEL (No Lambda)",
      "url": "https://gabrielkoo.com/blog/resurface-claude-code-usage-across-your-team-with-cloudwatch-otel-no-lambda-4p0i/",
      "canonical_url": "https://dev.to/aws-builders/resurface-claude-code-usage-across-your-team-with-cloudwatch-otel-no-lambda-4p0i",
      "published_at": "2026-04-18",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "opentelemetry",
        "cloudwatch",
        "serverless"
      ],
      "description": "I've been building AI tooling infrastructure to empower a team of 50+ software engineers to do vibe...",
      "content": "I've been building AI tooling infrastructure to empower a team of 50+ software engineers to do vibe coding safely. We went from 3 engineers using AI full-time to 50+ in 6 months — including non-engineers. (I co-presented on this journey at [AgentCon Hong Kong 2026](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/).)\n\nOne thing we learned: **you can't improve what you can't measure.** Once you give a team AI coding tools, you want visibility into how they're being used — not to evaluate individual engineers, but to understand adoption patterns. Which tools are people reaching for? How large are the prompts? What tool calls are being made? Making these metrics visible to everyone helps the team learn from each other and helps champions pull others forward.\n\nThis post is about the plumbing: how to get that telemetry data from coding agents into CloudWatch with minimal infrastructure.\n\n**\"But we already have an LLM gateway.\"** If your team routes AI traffic through a gateway like [LiteLLM](https://github.com/BerriAI/litellm) or [AWS Bedrock](https://aws.amazon.com/bedrock/), you already have token-level usage data. But if your engineers are on coding plans — Claude Team/Max, OpenCode Go, GitHub Copilot seats, ChatGPT Codex — the LLM calls bypass your gateway entirely. You lose visibility into the interesting stuff: how many tool calls per session, prompt sizes, which tools are being invoked, who's active at what times. That's where OTEL telemetry fills the gap.\n\nAI coding tools are shipping with built-in OpenTelemetry support. [Claude Code](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage), [Claude CoWork](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry), [GitHub Copilot](https://docs.github.com/copilot/how-tos/copilot-sdk/observability/opentelemetry), [Gemini CLI](https://geminicli.com/docs/cli/telemetry/), and [Cursor](https://github.com/LangGuard-AI/cursor-otel-hook) (via hooks) all export metrics, traces, and log events over OTLP/HTTP — token counts, tool durations, model latency, the works. [Kiro has an open feature request](https://github.com/kirodotdev/Kiro/issues/6319) for native OTEL support too.\n\nThere's one catch: **CloudWatch's OTLP endpoints require SigV4 signing.** These tools' OTEL SDKs can't do that. Neither can most OTEL SDKs without an AWS-specific exporter or a collector sidecar.\n\nThe usual fix is a Lambda function that receives OTLP, signs it, and forwards it. That means cold starts, packaging, and another thing to maintain.\n\nHere's a simpler way: **API Gateway REST API with AWS Service Integration.** APIGW signs the request with SigV4 using an execution role. No Lambda. No collector. No code.\n\n![Expanding Brain Meme](/assets/img/51d37c7f020b.png)\n\n> **Timeline:** CloudWatch has supported OTLP ingestion for [traces and logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) for some time (availability varies by region — check the [OTLP endpoints doc](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)). [Native OTLP metrics support launched April 2, 2026](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) in public preview, completing all three pillars of observability via OTLP.\n\n## Architecture\n\n![Architecture](/assets/img/91657a8ac5b6.png)\n\n```plaintext\nAI Coding Tool (OTEL SDK)\n  ↓ OTLP/HTTP + x-api-key\nAPI Gateway REST API\n  ├→ POST /v1/metrics  → AWS Integration → monitoring (SigV4) → CloudWatch Metrics\n  ├→ POST /v1/traces   → AWS Integration → xray (SigV4)      → X-Ray / CloudWatch Logs\n  └→ POST /v1/logs     → AWS Integration → logs (SigV4)       → CloudWatch Logs\n```\n\nThe client sends standard OTLP/HTTP requests with an API key. APIGW validates the key, assumes an IAM role, signs the request with SigV4, and forwards it to the CloudWatch OTLP endpoint. That's it.\n\n## Why This Works\n\nAPI Gateway REST API has an integration type called **[AWS Service Integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-aws-proxy.html)**. It can call any AWS service API and sign the request with SigV4 using an execution role. The [CloudWatch OTLP endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) are standard AWS service endpoints:\n\n| Signal | Endpoint | Service |\n|--------|----------|---------|\n| Metrics | `monitoring.{region}.amazonaws.com/v1/metrics` | `monitoring` |\n| Traces | `xray.{region}.amazonaws.com/v1/traces` | `xray` |\n| Logs | `logs.{region}.amazonaws.com/v1/logs` | `logs` |\n\nAPIGW's integration URI format maps directly:\n\n```plaintext\narn:aws:apigateway:{region}:monitoring:path/v1/metrics\narn:aws:apigateway:{region}:xray:path/v1/traces\narn:aws:apigateway:{region}:logs:path/v1/logs\n```\n\n## Setup\n\nThe full infrastructure is defined in a CloudFormation template (link at the bottom). Here's what it creates:\n\n### IAM Execution Role\n\nAPIGW needs an IAM role to sign requests to CloudWatch. The policy is scoped to only the actions and resources needed for OTLP ingestion:\n\n```yaml\nParameters:\n  OtlpLogGroupName:\n    Type: String\n    Default: \"otlp-logs\"\n    Description: CloudWatch Logs log group for OTLP log ingestion\n\nResources:\n  OtlpExecutionRole:\n    Type: AWS::IAM::Role\n    Properties:\n      AssumeRolePolicyDocument:\n        Statement:\n          - Effect: Allow\n            Principal:\n              Service: apigateway.amazonaws.com\n            Action: sts:AssumeRole\n      Policies:\n        - PolicyName: otlp-metrics\n          PolicyDocument:\n            Statement:\n              - Effect: Allow\n                Action:\n                  - cloudwatch:PutMetricData\n                Resource: \"*\"\n        - PolicyName: otlp-traces\n          PolicyDocument:\n            Statement:\n              - Effect: Allow\n                Action:\n                  - xray:PutTraceSegments\n                  - xray:PutTelemetryRecords\n                Resource: \"*\"\n        - PolicyName: otlp-logs\n          PolicyDocument:\n            Statement:\n              - Effect: Allow\n                Action:\n                  - logs:PutLogEvents\n                  - logs:CreateLogStream\n                  - logs:DescribeLogStreams\n                Resource:\n                  - !Sub \"arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${OtlpLogGroupName}:*\"\n```\n\nNote: `cloudwatch:PutMetricData` doesn't support resource-level ARNs. The `cloudwatch:namespace` condition key exists but does not apply to the OTLP ingestion path — metrics are accepted regardless of namespace. X-Ray `PutTraceSegments` also doesn't support resource-level restrictions. Logs permissions are scoped to a specific log group via the `OtlpLogGroupName` parameter.\n\n### API Gateway with AWS Service Integration\n\nEach OTLP signal gets its own resource with an AWS integration:\n\n```yaml\nMetricsMethod:\n  Type: AWS::ApiGateway::Method\n  Properties:\n    HttpMethod: POST\n    AuthorizationType: NONE\n    ApiKeyRequired: true\n    Integration:\n      Type: AWS\n      IntegrationHttpMethod: POST\n      Uri: !Sub \"arn:aws:apigateway:${AWS::Region}:monitoring:path/v1/metrics\"\n      Credentials: !GetAtt OtlpExecutionRole.Arn\n      PassthroughBehavior: WHEN_NO_MATCH\n      ContentHandling: CONVERT_TO_TEXT\n```\n\nSame pattern for `/v1/traces` (service: `xray`) and `/v1/logs` (service: `logs`).\n\n### API Key Authentication\n\nProtect the endpoint with an API key so only your tools can send telemetry:\n\n```yaml\nApiKey:\n  Type: AWS::ApiGateway::ApiKey\n  Properties:\n    Enabled: true\n\nUsagePlan:\n  Type: AWS::ApiGateway::UsagePlan\n  Properties:\n    ApiStages:\n      - ApiId: !Ref Api\n        Stage: !Ref Stage\n```\n\n## Configure Your Tools\n\nThe proxy works with any tool that supports standard OTEL environment variables. Here's how to configure each:\n\n> **Disclaimer:** I personally use Claude Code routed through a custom LLM gateway (not a coding plan), since some coding plans aren't available in the region I live in. The configurations below are based on each tool's official documentation — your mileage may vary.\n\n### Claude Code\n\n[Official monitoring docs](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage)\n\n```bash\nexport CLAUDE_CODE_ENABLE_TELEMETRY=1\nexport OTEL_METRICS_EXPORTER=otlp\nexport OTEL_LOGS_EXPORTER=otlp\nexport OTEL_EXPORTER_OTLP_PROTOCOL=http/json\nexport OTEL_EXPORTER_OTLP_ENDPOINT=https://xxx.execute-api.us-west-2.amazonaws.com/prod\nexport OTEL_EXPORTER_OTLP_HEADERS=x-api-key=your-api-key\nexport OTEL_SERVICE_NAME=claude-code\n```\n\nFor short-lived tasks, lower the export interval so data flushes before the process exits:\n\n```bash\nexport OTEL_METRIC_EXPORT_INTERVAL=1000\nexport OTEL_LOGS_EXPORT_INTERVAL=1000\n```\n\nFor traces (beta):\n\n```bash\nexport CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1\nexport OTEL_TRACES_EXPORTER=otlp\nexport OTEL_TRACES_EXPORT_INTERVAL=1000\n```\n\n**Enforcing OTEL across your team:** Claude Code supports [managed settings](https://code.claude.com/docs/en/settings#settings-files) via `managed-settings.json`, deployable through MDM (Jamf, Intune, etc.). This lets you enforce OTEL configuration org-wide — engineers don't need to set environment variables manually, and they can't opt out.\n\n### Claude CoWork (Team & Enterprise)\n\n[CoWork monitoring docs](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) — configure via Admin Settings → Cowork → Monitoring:\n\n- OTLP endpoint: your APIGW URL\n- OTLP protocol: `http/json`\n- OTLP headers: `x-api-key=your-api-key`\n\nCoWork streams user prompts, tool/MCP invocations, file access, human approval decisions, and API request details. It shares the same OTel event schema as Claude Code via the Claude Agent SDK — you can distinguish them by `terminal.type` (`cowork` vs `cli`).\n\n### GitHub Copilot CLI\n\n[Copilot CLI OTel reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference#opentelemetry-monitoring) — available since Copilot CLI 1.0.4:\n\n```bash\nexport COPILOT_OTEL_ENDPOINT=https://xxx.execute-api.us-west-2.amazonaws.com/prod\nexport COPILOT_OTEL_HEADERS=x-api-key=your-api-key\n```\n\n### Gemini CLI\n\n[Gemini CLI telemetry docs](https://geminicli.com/docs/cli/telemetry/)\n\n```bash\nexport GEMINI_CLI_OTEL_EXPORT_ENDPOINT=https://xxx.execute-api.us-west-2.amazonaws.com/prod\n```\n\n### Cursor (via Hooks)\n\nCursor doesn't have native OTEL export yet, but the community [cursor-otel-hook](https://github.com/LangGuard-AI/cursor-otel-hook) project captures agent activity via Cursor's hook system and exports traces to any OTLP endpoint. Configure via `otel_config.json`:\n\n```json\n{\n  \"OTEL_EXPORTER_OTLP_ENDPOINT\": \"https://xxx.execute-api.us-west-2.amazonaws.com/prod/v1/traces\",\n  \"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/json\",\n  \"OTEL_EXPORTER_OTLP_HEADERS\": { \"x-api-key\": \"your-api-key\" }\n}\n```\n\n## What You Get\n\nCloudWatch receives standard OTLP data. For Claude Code specifically:\n\n- **Metrics**: `claude_code.token.usage` (by `token.type`: input/output/cache_read/cache_creation), `claude_code.cost.usage` (USD), `claude_code.session.count`, `claude_code.lines_of_code.count`\n- **Traces** (beta): Spans linking each user prompt → API requests → tool executions\n- **Log events**: `claude_code.user_prompt`, `claude_code.tool_decision`, `claude_code.tool_result`, `claude_code.api_request` — all tagged with `session.id` and `service.name=claude-code`\n\nHere's what real Claude Code log events look like after flowing through the proxy into CloudWatch Logs. This is actual data from an E2E test — a single prompt that triggered a Bash tool call:\n\n**`claude_code.user_prompt`** — emitted when the user sends a prompt:\n\n```json\n{\n  \"resource\": {\n    \"attributes\": {\n      \"host.arch\": \"arm64\",\n      \"os.type\": \"linux\",\n      \"service.name\": \"claude-code\",\n      \"service.version\": \"2.1.114\",\n      \"os.version\": \"6.17.0-1010-aws\"\n    }\n  },\n  \"scope\": {\n    \"name\": \"com.anthropic.claude_code.events\",\n    \"version\": \"2.1.114\"\n  },\n  \"body\": \"claude_code.user_prompt\",\n  \"attributes\": {\n    \"event.sequence\": 0,\n    \"user.id\": \"1c257d04...\",\n    \"prompt_length\": \"40\",\n    \"terminal.type\": \"non-interactive\",\n    \"event.name\": \"user_prompt\",\n    \"event.timestamp\": \"2026-04-18T11:23:13.187Z\",\n    \"prompt\": \"<REDACTED>\",\n    \"session.id\": \"846ab649-8bba-471e-8ec5-8756116d0840\",\n    \"prompt.id\": \"88475ee2-59c2-4137-9201-5540c6a6cad1\"\n  }\n}\n```\n\n**`claude_code.tool_result`** — emitted after each tool execution:\n\n```json\n{\n  \"body\": \"claude_code.tool_result\",\n  \"attributes\": {\n    \"tool_name\": \"Bash\",\n    \"tool_result_size_bytes\": \"899\",\n    \"tool_input\": \"{\\\"command\\\":\\\"ls\\\",\\\"description\\\":\\\"List files in current directory\\\"}\",\n    \"duration_ms\": \"95\",\n    \"success\": \"true\",\n    \"session.id\": \"846ab649-8bba-471e-8ec5-8756116d0840\",\n    \"prompt.id\": \"88475ee2-59c2-4137-9201-5540c6a6cad1\"\n  }\n}\n```\n\n**`claude_code.api_request`** — emitted after each API call with token counts and cost:\n\n```json\n{\n  \"body\": \"claude_code.api_request\",\n  \"attributes\": {\n    \"model\": \"claude-sonnet-4-5-20250929\",\n    \"input_tokens\": \"142\",\n    \"output_tokens\": \"61\",\n    \"cache_read_tokens\": \"0\",\n    \"cache_creation_tokens\": \"25848\",\n    \"cost_usd\": \"0.098271\",\n    \"duration_ms\": \"4950\",\n    \"speed\": \"normal\",\n    \"session.id\": \"846ab649-8bba-471e-8ec5-8756116d0840\",\n    \"prompt.id\": \"88475ee2-59c2-4137-9201-5540c6a6cad1\"\n  }\n}\n```\n\nAll events share the same `prompt.id`, linking them into a single interaction. The `event.sequence` field orders events within a prompt. Every record carries `service.name=claude-code` in resource attributes, so isolating Claude Code telemetry in a mixed pipeline is trivial — just filter on that in CloudWatch Logs Insights:\n\n```sql\nfields @timestamp, body, attributes.model, attributes.cost_usd, attributes.duration_ms\n| filter resource.attributes.`service.name` = 'claude-code'\n| filter body = 'claude_code.api_request'\n| sort @timestamp desc\n```\n\n## Region Availability\n\nCloudWatch OTLP endpoints are available in most regions but **not all**. The [OTLP metrics preview](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) launched in 5 regions:\n\n| Signal | Regions | Docs |\n|--------|---------|------|\n| Metrics (preview) | us-east-1, us-west-2, ap-southeast-1, ap-southeast-2, eu-west-1 | [Announcement](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) |\n| Traces | Most commercial regions | [OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) |\n| Logs | Most commercial regions | [OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) |\n\nTested and confirmed:\n\n| Region | Metrics | Traces | Logs |\n|--------|---------|--------|------|\n| us-east-1 | ✅ | ✅ | ✅ |\n| us-west-2 | ✅ | ✅ | ✅ |\n| ap-southeast-1 | ✅ | ✅ | ✅ |\n| ap-east-1 (Hong Kong) | ❌ | ❌ | ❌ |\n\nIf your primary region doesn't support it, deploy the proxy in a supported region. The APIGW endpoint is accessible from anywhere.\n\nFor the full list of CloudWatch service endpoints by region, see the [AWS General Reference](https://docs.aws.amazon.com/general/latest/gr/cw_region.html).\n\n## Gotchas\n\n**XRay traces require manual setup.** The CloudFormation template creates the proxy endpoints, but X-Ray traces need two additional steps that aren't in the template:\n\n1. Set CloudWatch Logs as the trace segment destination:\n\n```bash\naws xray update-trace-segment-destination --destination CloudWatchLogs\n```\n\n2. Create a CloudWatch Logs resource policy allowing X-Ray to write to the `aws/spans` log group:\n\n```bash\naws logs put-resource-policy \\\n  --policy-name XRayAccessPolicy \\\n  --policy-document '{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"xray.amazonaws.com\"},\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogGroup\",\"logs:CreateLogStream\"],\"Resource\":\"*\"}]}'\n```\n\nWithout these, traces will return `AccessDeniedException`.\n\n**CloudWatch Logs supports bearer token auth.** The `/v1/logs` endpoint supports [bearer token authentication](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) without SigV4 — but only for logs. Metrics and traces still require SigV4, which is why the APIGW proxy is needed for a unified endpoint.\n\n**Use `http/json`, not `http/protobuf`.** CloudWatch accepts both formats, but API Gateway's `CONVERT_TO_TEXT` content handling can corrupt binary protobuf payloads in transit. Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/json` to avoid this. JSON is also easier to debug in APIGW execution logs. Most coding tools default to protobuf, so you'll need to override this explicitly.\n\n**API Gateway payload limit.** REST API has a 10MB payload limit. OTLP batches from coding tools are well under this, but keep it in mind if you're aggregating from multiple sources. CloudWatch's own limits are 1MB for metrics and logs, 5MB for traces ([full limits](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)).\n\n**REST API, not HTTP API.** Only REST API supports the `AWS` integration type needed for SigV4 service proxying. HTTP API does not.\n\n## Cost\n\nThis is about as cheap as it gets for a telemetry pipeline:\n\n| Component | Cost |\n|-----------|------|\n| API Gateway | ~$3.50 / million requests |\n| CloudWatch Metrics | [Standard CW pricing](https://aws.amazon.com/cloudwatch/pricing/) (free during OTel metrics preview) |\n| CloudWatch Logs | [Standard CW pricing](https://aws.amazon.com/cloudwatch/pricing/) |\n| Lambda | $0 (there is none) |\n\nNo idle cost. No provisioned capacity. Pure pay-per-request.\n\nFor comparison: a Lambda-based OTLP forwarder would add ~$0.20/million invocations plus compute time, but gives you retry logic and transformation capabilities. At typical coding agent volumes (a few hundred requests/day per developer), the cost difference is negligible — the real win is operational simplicity.\n\n## When NOT to Use This\n\nThis proxy is optimized for simplicity. It's the right choice for low-to-moderate telemetry volumes from coding agents and developer tools. But it has tradeoffs:\n\n| Approach | Complexity | Cost | Retries | Multi-destination | Transformation |\n|----------|-----------|------|---------|-------------------|----------------|\n| This proxy (APIGW) | Minimal | ~$3.50/M req | ❌ | ❌ | ❌ |\n| OTel Collector | Medium | Compute cost | ✅ | ✅ | ✅ |\n| Lambda forwarder | Medium | ~$0.20/M + compute | ✅ | ✅ | ✅ |\n| ADOT SDK (in-app) | Low | Free (SigV4 native) | ✅ | ❌ | ❌ |\n| SaaS (Datadog, etc.) | Low | $$$ | ✅ | N/A | ✅ |\n\nConsider an OTel Collector or Lambda forwarder instead if you need:\n\n- **High throughput** — thousands of requests/second from many sources\n- **Retry and buffering** — this proxy is fire-and-forget; if CloudWatch returns an error, the data is lost. OTEL SDKs have built-in retry, but only for transient failures\n- **Multi-destination routing** — fan out to CloudWatch + Datadog + S3 simultaneously\n- **Payload transformation** — filter, enrich, or redact telemetry before ingestion\n- **Compliance requirements** — audit trails, guaranteed delivery, or data residency controls\n\nFor most coding agent monitoring use cases (a team of 5-50 developers), this proxy handles the volume comfortably.\n\n## Security Considerations\n\nThe proxy uses API key authentication — simple but not the strongest option. Here's how to harden it:\n\n**Attach AWS WAF to the REST API.** Add rate limiting, IP allowlisting, or geo-blocking to prevent abuse. A single WAF WebACL with a rate-based rule (e.g., 1000 req/5min per IP) costs ~$6/month and stops most abuse patterns.\n\n**Rotate API keys.** APIGW supports multiple API keys per usage plan. Create a new key, distribute it, then disable the old one — zero downtime rotation.\n\n**Consider IAM auth for internal use.** If your tools run inside AWS (EC2, ECS, Lambda), switch `AuthorizationType` from `NONE` to `AWS_IAM` and drop the API key entirely. The caller signs requests with SigV4 using their IAM role — no shared secrets. This doesn't work for external tools like Claude Code on developer laptops, but it's ideal for CI/CD pipelines or server-side agents.\n\n**Egress control.** If you're running coding agents in a controlled environment, restrict outbound traffic to only your APIGW endpoint. This prevents telemetry from leaking to unauthorized collectors.\n\n## Beyond Coding Agents\n\nThis proxy works with **any OTEL SDK** that supports OTLP/HTTP. If your tool can set `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_HEADERS`, it can ship telemetry to CloudWatch through this proxy.\n\nPotential use cases:\n- **AI coding agents** (Claude Code, CoWork, Copilot, Cursor, Gemini CLI) — track token usage, costs, and tool calls across your org\n- **Internal tools** — ship metrics without embedding AWS credentials in client apps\n- **CI/CD pipelines** — export build/test telemetry to CloudWatch\n- **On-premises services** — send OTLP from outside AWS without running ADOT Collector\n\nFor apps running inside AWS with IAM roles available, consider the [ADOT SDK](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLP-UsingADOT.html) for collector-less telemetry with native SigV4 signing — no proxy needed.\n\n## Source Code & One-Click Deploy\n\nThe CloudFormation template and full documentation are on GitHub:\n\n👉 [gabrielkoo/otlp-cloudwatch-proxy](https://github.com/gabrielkoo/otlp-cloudwatch-proxy)\n\nOne-click deploy to supported regions:\n\n| Region | Deploy |\n|--------|--------|\n| US East (N. Virginia) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) |\n| US West (Oregon) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) |\n| Asia Pacific (Singapore) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-1#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) |\n| Asia Pacific (Sydney) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) |\n| Europe (Ireland) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) |\n\n---\n\n*Built and validated on a Saturday morning with Claude Code + OpenClaw. Zero Lambda functions were harmed in the making of this article.*\n\n## Further Reading\n\n- **[AWS Guidance for Claude Code with Amazon Bedrock — Monitoring](https://github.com/aws-solutions-library-samples/guidance-for-claude-code-with-amazon-bedrock/blob/main/assets/docs/MONITORING.md)** — A comprehensive (and admittedly overkill) reference implementation using ECS Fargate + ALB + ADOT Collector + Lambda + DynamoDB + Kinesis + Athena. Great if you want to see the full spectrum of what can be measured: per-user token tracking, quota monitoring, cost dashboards, and an analytics data lake. If you need all of that, use it. If you just need telemetry flowing to CloudWatch, the one-template proxy in this post will do.\n- **[Claude Code Monitoring Docs](https://code.claude.com/docs/en/monitoring-usage)** — Official OTEL configuration reference, including all metrics, events, and traces.\n- **[Claude Code Managed Settings](https://code.claude.com/docs/en/settings#settings-files)** — How to deploy `managed-settings.json` via MDM for org-wide OTEL enforcement.\n- **[CloudWatch OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)** — AWS docs on native OTLP ingestion for metrics, traces, and logs.",
      "excerpts": [
        "I've been building AI tooling infrastructure to empower a team of 50+ software engineers to do vibe coding safely. We went from 3 engineers using AI full-time to 50+ in 6 months — including non-engineers. (I co-presented on this journey at [AgentCon Hong Kong 2026](https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/).)",
        "One thing we learned: **you can't improve what you can't measure.** Once you give a team AI coding tools, you want visibility into how they're being used — not to evaluate individual engineers, but to understand adoption patterns. Which tools are people reaching for? How large are the prompts? What tool calls are being made? Making these metrics visible to everyone helps the team learn from each other and helps champions pull others forward.",
        "This post is about the plumbing: how to get that telemetry data from coding agents into CloudWatch with minimal infrastructure.",
        "**\"But we already have an LLM gateway.\"** If your team routes AI traffic through a gateway like [LiteLLM](https://github.com/BerriAI/litellm) or [AWS Bedrock](https://aws.amazon.com/bedrock/), you already have token-level usage data. But if your engineers are on coding plans — Claude Team/Max, OpenCode Go, GitHub Copilot seats, ChatGPT Codex — the LLM calls bypass your gateway entirely. You lose visibility into the interesting stuff: how many tool calls per session, prompt sizes, which tools are being invoked, who's active at what times. That's where OTEL telemetry fills the gap.",
        "AI coding tools are shipping with built-in OpenTelemetry support. [Claude Code](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage), [Claude CoWork](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry), [GitHub Copilot](https://docs.github.com/copilot/how-tos/copilot-sdk/observability/opentelemetry), [Gemini CLI](https://geminicli.com/docs/cli/telemetry/), and [Cursor](https://github.com/LangGuard-AI/cursor-otel-hook) (via hooks) all export metrics, traces, and log events over OTLP/HTTP — token counts, tool durations, model latency, the works. [Kiro has an open feature request](https://github.com/kirodotdev/Kiro/issues/6319) for native OTEL support too.",
        "There's one catch: **CloudWatch's OTLP endpoints require SigV4 signing.** These tools' OTEL SDKs can't do that. Neither can most OTEL SDKs without an AWS-specific exporter or a collector sidecar.",
        "The usual fix is a Lambda function that receives OTLP, signs it, and forwards it. That means cold starts, packaging, and another thing to maintain.",
        "Here's a simpler way: **API Gateway REST API with AWS Service Integration.** APIGW signs the request with SigV4 using an execution role. No Lambda. No collector. No code.",
        "![Expanding Brain Meme](/assets/img/51d37c7f020b.png)",
        "> **Timeline:** CloudWatch has supported OTLP ingestion for [traces and logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) for some time (availability varies by region — check the [OTLP endpoints doc](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)). [Native OTLP metrics support launched April 2, 2026](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) in public preview, completing all three pillars of observability via OTLP.",
        "![Architecture](/assets/img/91657a8ac5b6.png)",
        "The client sends standard OTLP/HTTP requests with an API key. APIGW validates the key, assumes an IAM role, signs the request with SigV4, and forwards it to the CloudWatch OTLP endpoint. That's it.",
        "API Gateway REST API has an integration type called **[AWS Service Integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-aws-proxy.html)**. It can call any AWS service API and sign the request with SigV4 using an execution role. The [CloudWatch OTLP endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) are standard AWS service endpoints:",
        "| Signal | Endpoint | Service | |--------|----------|---------| | Metrics | `monitoring.{region}.amazonaws.com/v1/metrics` | `monitoring` | | Traces | `xray.{region}.amazonaws.com/v1/traces` | `xray` | | Logs | `logs.{region}.amazonaws.com/v1/logs` | `logs` |",
        "APIGW's integration URI format maps directly:",
        "The full infrastructure is defined in a CloudFormation template (link at the bottom). Here's what it creates:",
        "APIGW needs an IAM role to sign requests to CloudWatch. The policy is scoped to only the actions and resources needed for OTLP ingestion:",
        "Resources: OtlpExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: apigateway.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: otlp-metrics PolicyDocument: Statement: - Effect: Allow Action: - cloudwatch:PutMetricData Resource: \"*\" - PolicyName: otlp-traces PolicyDocument: Statement: - Effect: Allow Action: - xray:PutTraceSegments - xray:PutTelemetryRecords Resource: \"*\" - PolicyName: otlp-logs PolicyDocument: Statement: - Effect: Allow Action: - logs:PutLogEvents - logs:CreateLogStream - logs:DescribeLogStreams Resource: - !Sub \"arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${OtlpLogGroupName}:*\" ```",
        "Note: `cloudwatch:PutMetricData` doesn't support resource-level ARNs. The `cloudwatch:namespace` condition key exists but does not apply to the OTLP ingestion path — metrics are accepted regardless of namespace. X-Ray `PutTraceSegments` also doesn't support resource-level restrictions. Logs permissions are scoped to a specific log group via the `OtlpLogGroupName` parameter.",
        "API Gateway with AWS Service Integration",
        "Each OTLP signal gets its own resource with an AWS integration:",
        "Same pattern for `/v1/traces` (service: `xray`) and `/v1/logs` (service: `logs`).",
        "Protect the endpoint with an API key so only your tools can send telemetry:",
        "UsagePlan: Type: AWS::ApiGateway::UsagePlan Properties: ApiStages: - ApiId: !Ref Api Stage: !Ref Stage ```",
        "The proxy works with any tool that supports standard OTEL environment variables. Here's how to configure each:",
        "> **Disclaimer:** I personally use Claude Code routed through a custom LLM gateway (not a coding plan), since some coding plans aren't available in the region I live in. The configurations below are based on each tool's official documentation — your mileage may vary.",
        "[Official monitoring docs](https://docs.anthropic.com/en/docs/claude-code/monitoring-usage)",
        "For short-lived tasks, lower the export interval so data flushes before the process exits:",
        "**Enforcing OTEL across your team:** Claude Code supports [managed settings](https://code.claude.com/docs/en/settings#settings-files) via `managed-settings.json`, deployable through MDM (Jamf, Intune, etc.). This lets you enforce OTEL configuration org-wide — engineers don't need to set environment variables manually, and they can't opt out.",
        "Claude CoWork (Team & Enterprise)",
        "[CoWork monitoring docs](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) — configure via Admin Settings → Cowork → Monitoring:",
        "- OTLP endpoint: your APIGW URL - OTLP protocol: `http/json` - OTLP headers: `x-api-key=your-api-key`",
        "CoWork streams user prompts, tool/MCP invocations, file access, human approval decisions, and API request details. It shares the same OTel event schema as Claude Code via the Claude Agent SDK — you can distinguish them by `terminal.type` (`cowork` vs `cli`).",
        "[Copilot CLI OTel reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference#opentelemetry-monitoring) — available since Copilot CLI 1.0.4:",
        "[Gemini CLI telemetry docs](https://geminicli.com/docs/cli/telemetry/)",
        "Cursor doesn't have native OTEL export yet, but the community [cursor-otel-hook](https://github.com/LangGuard-AI/cursor-otel-hook) project captures agent activity via Cursor's hook system and exports traces to any OTLP endpoint. Configure via `otel_config.json`:",
        "CloudWatch receives standard OTLP data. For Claude Code specifically:",
        "- **Metrics**: `claude_code.token.usage` (by `token.type`: input/output/cache_read/cache_creation), `claude_code.cost.usage` (USD), `claude_code.session.count`, `claude_code.lines_of_code.count` - **Traces** (beta): Spans linking each user prompt → API requests → tool executions - **Log events**: `claude_code.user_prompt`, `claude_code.tool_decision`, `claude_code.tool_result`, `claude_code.api_request` — all tagged with `session.id` and `service.name=claude-code`",
        "Here's what real Claude Code log events look like after flowing through the proxy into CloudWatch Logs. This is actual data from an E2E test — a single prompt that triggered a Bash tool call:",
        "**`claude_code.user_prompt`** — emitted when the user sends a prompt:",
        "**`claude_code.tool_result`** — emitted after each tool execution:",
        "**`claude_code.api_request`** — emitted after each API call with token counts and cost:",
        "All events share the same `prompt.id`, linking them into a single interaction. The `event.sequence` field orders events within a prompt. Every record carries `service.name=claude-code` in resource attributes, so isolating Claude Code telemetry in a mixed pipeline is trivial — just filter on that in CloudWatch Logs Insights:",
        "CloudWatch OTLP endpoints are available in most regions but **not all**. The [OTLP metrics preview](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) launched in 5 regions:",
        "| Signal | Regions | Docs | |--------|---------|------| | Metrics (preview) | us-east-1, us-west-2, ap-southeast-1, ap-southeast-2, eu-west-1 | [Announcement](https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-cloudwatch-opentelemetry-metrics/) | | Traces | Most commercial regions | [OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) | | Logs | Most commercial regions | [OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) |",
        "| Region | Metrics | Traces | Logs | |--------|---------|--------|------| | us-east-1 | ✅ | ✅ | ✅ | | us-west-2 | ✅ | ✅ | ✅ | | ap-southeast-1 | ✅ | ✅ | ✅ | | ap-east-1 (Hong Kong) | ❌ | ❌ | ❌ |",
        "If your primary region doesn't support it, deploy the proxy in a supported region. The APIGW endpoint is accessible from anywhere.",
        "For the full list of CloudWatch service endpoints by region, see the [AWS General Reference](https://docs.aws.amazon.com/general/latest/gr/cw_region.html).",
        "**XRay traces require manual setup.** The CloudFormation template creates the proxy endpoints, but X-Ray traces need two additional steps that aren't in the template:",
        "1. Set CloudWatch Logs as the trace segment destination:",
        "2. Create a CloudWatch Logs resource policy allowing X-Ray to write to the `aws/spans` log group:",
        "Without these, traces will return `AccessDeniedException`.",
        "**CloudWatch Logs supports bearer token auth.** The `/v1/logs` endpoint supports [bearer token authentication](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html) without SigV4 — but only for logs. Metrics and traces still require SigV4, which is why the APIGW proxy is needed for a unified endpoint.",
        "**Use `http/json`, not `http/protobuf`.** CloudWatch accepts both formats, but API Gateway's `CONVERT_TO_TEXT` content handling can corrupt binary protobuf payloads in transit. Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/json` to avoid this. JSON is also easier to debug in APIGW execution logs. Most coding tools default to protobuf, so you'll need to override this explicitly.",
        "**API Gateway payload limit.** REST API has a 10MB payload limit. OTLP batches from coding tools are well under this, but keep it in mind if you're aggregating from multiple sources. CloudWatch's own limits are 1MB for metrics and logs, 5MB for traces ([full limits](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)).",
        "**REST API, not HTTP API.** Only REST API supports the `AWS` integration type needed for SigV4 service proxying. HTTP API does not.",
        "This is about as cheap as it gets for a telemetry pipeline:",
        "| Component | Cost | |-----------|------| | API Gateway | ~$3.50 / million requests | | CloudWatch Metrics | [Standard CW pricing](https://aws.amazon.com/cloudwatch/pricing/) (free during OTel metrics preview) | | CloudWatch Logs | [Standard CW pricing](https://aws.amazon.com/cloudwatch/pricing/) | | Lambda | $0 (there is none) |",
        "No idle cost. No provisioned capacity. Pure pay-per-request.",
        "For comparison: a Lambda-based OTLP forwarder would add ~$0.20/million invocations plus compute time, but gives you retry logic and transformation capabilities. At typical coding agent volumes (a few hundred requests/day per developer), the cost difference is negligible — the real win is operational simplicity.",
        "This proxy is optimized for simplicity. It's the right choice for low-to-moderate telemetry volumes from coding agents and developer tools. But it has tradeoffs:",
        "| Approach | Complexity | Cost | Retries | Multi-destination | Transformation | |----------|-----------|------|---------|-------------------|----------------| | This proxy (APIGW) | Minimal | ~$3.50/M req | ❌ | ❌ | ❌ | | OTel Collector | Medium | Compute cost | ✅ | ✅ | ✅ | | Lambda forwarder | Medium | ~$0.20/M + compute | ✅ | ✅ | ✅ | | ADOT SDK (in-app) | Low | Free (SigV4 native) | ✅ | ❌ | ❌ | | SaaS (Datadog, etc.) | Low | $$$ | ✅ | N/A | ✅ |",
        "Consider an OTel Collector or Lambda forwarder instead if you need:",
        "- **High throughput** — thousands of requests/second from many sources - **Retry and buffering** — this proxy is fire-and-forget; if CloudWatch returns an error, the data is lost. OTEL SDKs have built-in retry, but only for transient failures - **Multi-destination routing** — fan out to CloudWatch + Datadog + S3 simultaneously - **Payload transformation** — filter, enrich, or redact telemetry before ingestion - **Compliance requirements** — audit trails, guaranteed delivery, or data residency controls",
        "For most coding agent monitoring use cases (a team of 5-50 developers), this proxy handles the volume comfortably.",
        "The proxy uses API key authentication — simple but not the strongest option. Here's how to harden it:",
        "**Attach AWS WAF to the REST API.** Add rate limiting, IP allowlisting, or geo-blocking to prevent abuse. A single WAF WebACL with a rate-based rule (e.g., 1000 req/5min per IP) costs ~$6/month and stops most abuse patterns.",
        "**Rotate API keys.** APIGW supports multiple API keys per usage plan. Create a new key, distribute it, then disable the old one — zero downtime rotation.",
        "**Consider IAM auth for internal use.** If your tools run inside AWS (EC2, ECS, Lambda), switch `AuthorizationType` from `NONE` to `AWS_IAM` and drop the API key entirely. The caller signs requests with SigV4 using their IAM role — no shared secrets. This doesn't work for external tools like Claude Code on developer laptops, but it's ideal for CI/CD pipelines or server-side agents.",
        "**Egress control.** If you're running coding agents in a controlled environment, restrict outbound traffic to only your APIGW endpoint. This prevents telemetry from leaking to unauthorized collectors.",
        "This proxy works with **any OTEL SDK** that supports OTLP/HTTP. If your tool can set `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_HEADERS`, it can ship telemetry to CloudWatch through this proxy.",
        "Potential use cases: - **AI coding agents** (Claude Code, CoWork, Copilot, Cursor, Gemini CLI) — track token usage, costs, and tool calls across your org - **Internal tools** — ship metrics without embedding AWS credentials in client apps - **CI/CD pipelines** — export build/test telemetry to CloudWatch - **On-premises services** — send OTLP from outside AWS without running ADOT Collector",
        "For apps running inside AWS with IAM roles available, consider the [ADOT SDK](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLP-UsingADOT.html) for collector-less telemetry with native SigV4 signing — no proxy needed.",
        "Source Code & One-Click Deploy",
        "The CloudFormation template and full documentation are on GitHub:",
        "👉 [gabrielkoo/otlp-cloudwatch-proxy](https://github.com/gabrielkoo/otlp-cloudwatch-proxy)",
        "One-click deploy to supported regions:",
        "| Region | Deploy | |--------|--------| | US East (N. Virginia) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) | | US West (Oregon) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) | | Asia Pacific (Singapore) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-1#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml&stackName=otlp-cloudwatch-proxy) | | Asia Pacific (Sydney) | [![Launch Stack](/assets/img/4bf452529163.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/create/review?templateURL=https://raw.githubusercontent.com/gabrielkoo/otlp-cloudwatch-proxy/main/template.yaml",
        "*Built and validated on a Saturday morning with Claude Code + OpenClaw. Zero Lambda functions were harmed in the making of this article.*",
        "- **[AWS Guidance for Claude Code with Amazon Bedrock — Monitoring](https://github.com/aws-solutions-library-samples/guidance-for-claude-code-with-amazon-bedrock/blob/main/assets/docs/MONITORING.md)** — A comprehensive (and admittedly overkill) reference implementation using ECS Fargate + ALB + ADOT Collector + Lambda + DynamoDB + Kinesis + Athena. Great if you want to see the full spectrum of what can be measured: per-user token tracking, quota monitoring, cost dashboards, and an analytics data lake. If you need all of that, use it. If you just need telemetry flowing to CloudWatch, the one-template proxy in this post will do. - **[Claude Code Monitoring Docs](https://code.claude.com/docs/en/monitoring-usage)** — Official OTEL configuration reference, including all metrics, events, and traces. - **[Claude Code Managed Settings](https://code.claude.com/docs/en/settings#settings-files)** — How to deploy `managed-settings.json` via MDM for org-wide OTEL enforcement. - **[CloudWatch OTLP Endpoints](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html)** — AWS docs on native OTLP ingestion for metrics, traces, and logs."
      ]
    },
    {
      "id": "article:bedrock-for-ai-coding-tools-mantle-vs-gateway-vs-litellm-a-decision-guide-for-aws-credit-burners-1h01",
      "source_type": "article",
      "title": "Bedrock for AI Coding Tools: Mantle vs Gateway vs LiteLLM — A Decision Guide for AWS Credit Burners",
      "url": "https://gabrielkoo.com/blog/bedrock-for-ai-coding-tools-mantle-vs-gateway-vs-litellm-a-decision-guide-for-aws-credit-burners-1h01/",
      "canonical_url": "https://dev.to/aws-builders/bedrock-for-ai-coding-tools-mantle-vs-gateway-vs-litellm-a-decision-guide-for-aws-credit-burners-1h01",
      "published_at": "2026-03-22",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "bedrock",
        "ai",
        "openai"
      ],
      "description": "You have AWS credits. You want to use them on AI coding tools — OpenCode, Codex CLI, Claude Code,...",
      "content": "You have AWS credits. You want to use them on AI coding tools — OpenCode, Codex CLI, Claude Code, whatever. Amazon Bedrock has the models. But how do you actually connect them?\n\nThere are three approaches, and picking the wrong one wastes time. Here's the decision guide I wish I had.\n\n> All data in this post is as of March 2026. Model counts and API support may change — check [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io) for the latest.\n\n## TL;DR\n\n- **Just want it to work?** Mantle + OpenCode. Five minutes, zero infra.\n- **Need Claude models via OpenAI API?** bedrock-access-gateway on Lambda.\n- **Need Claude Code specifically?** LiteLLM. It's the only path.\n- **Codex CLI?** Broken with all three. Wait for LiteLLM to fix a tool translation bug.\n\n## The three paths\n\n![Decision flowchart: Mantle vs bedrock-access-gateway vs LiteLLM](/assets/img/5ec03a820bf7.png)\n\n**One thing all three have in common: your API keys and code context stay within your AWS account or your own infrastructure.** Third-party AI gateways exist (Bifrost, Portkey, etc.), but they require routing your Bedrock API keys and code context through someone else's servers. Self-hosted or AWS-native — that's the baseline.\n\n### 1. Bedrock Mantle — no self-hosted infra required\n\n[Mantle](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) is AWS's native OpenAI-compatible endpoint. No Lambda, no container, no proxy — just set your base URL and API key:\n\n```bash\nexport OPENAI_BASE_URL=\"https://bedrock-mantle.us-east-1.api.aws/v1\"\nexport OPENAI_API_KEY=\"your-bedrock-api-key\"\n```\n\n**What's on Mantle:** 38 open-weight models — DeepSeek, Mistral, Qwen, GLM, NVIDIA Nemotron, MiniMax, Moonshot Kimi, Google Gemma, OpenAI gpt-oss, and Writer Palmyra.\n\n**What's NOT on Mantle:** Anthropic Claude, Amazon Nova, Meta Llama, AI21, Cohere — the proprietary/first-party models are absent.\n\n**API coverage:** Mantle exposes Chat Completions (`/v1/chat/completions`) and Responses API (`/v1/responses`). No Anthropic Messages API (`/v1/messages`).\n\nThe Responses API is limited — only 4 models support it: `openai.gpt-oss-120b-1:0`, `openai.gpt-oss-20b-1:0`, `openai.gpt-oss-120b`, and `openai.gpt-oss-20b`. Every other model is Chat Completions only. I verified this by scraping all 102 model card pages in the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html).\n\n**Cost:** Standard Bedrock on-demand pricing. No gateway markup, no infra costs.\n\n**Best for:** OpenCode or any tool that speaks OpenAI Chat Completions.\n\n### 2. bedrock-access-gateway — self-hosted, all models\n\n[bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway) (or my fork, [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url)) gives you an OpenAI-compatible proxy backed by all Bedrock models — including Claude, Nova, and Llama.\n\nDeploy it as a Lambda Function URL or on ECS, and you get:\n\n```bash\nexport OPENAI_BASE_URL=\"https://your-lambda-url.lambda-url.us-west-2.on.aws/api/v1\"\nexport OPENAI_API_KEY=\"your-gateway-api-key\"\n```\n\nThe tradeoff: you maintain infrastructure. But you get access to every Bedrock model through a single OpenAI-compatible endpoint.\n\n**Cost:** Bedrock on-demand pricing + Lambda/ECS compute costs (minimal for Lambda Function URLs — you pay per invocation).\n\n**Best for:** When you need Claude or Nova through OpenAI-compatible tools, or want full control over routing, caching, and logging.\n\n### 3. LiteLLM — the universal translator\n\n[LiteLLM](https://github.com/BerriAI/litellm) is the Swiss Army knife. It translates between API schemas — OpenAI, Anthropic, Bedrock native, and more. It's the only option that gives you Anthropic Messages API (`/v1/messages`) compatibility with Bedrock models.\n\nThis matters because **Claude Code uses the Anthropic API schema**, not OpenAI's. If you want to run Claude Code against Bedrock, LiteLLM is your best (and arguably only) option. I tested this end-to-end: Claude Code CLI → LiteLLM → Bedrock Converse API — it works, including streaming responses.\n\nIs it perfect? No. Setup is more complex (Python process or Docker container, optional PostgreSQL for analytics), and you're adding another layer of abstraction. But it's the most flexible gateway available.\n\n**Cost:** Bedrock on-demand pricing + your compute costs for hosting LiteLLM. No per-call markup from LiteLLM itself (open source).\n\n**Best for:** Claude Code, or when you need both OpenAI and Anthropic API compatibility from a single proxy.\n\n## Tool compatibility matrix\n\n| Tool | API Schema | Mantle | bedrock-access-gateway | LiteLLM |\n|------|-----------|--------|----------------------|---------|\n| OpenCode | OpenAI Chat | ✅ | ✅ | ✅ |\n| Codex CLI | OpenAI Responses | ❌ Auth issues | ❌ No Responses API | ⚠️ Tool bug |\n| Claude Code | Anthropic Messages | ❌ No support | ❌ Wrong schema | ✅ |\n\nNote: Anthropic-native tools like Kiro CLI also work through LiteLLM's Anthropic Messages API translation.\n\n### A note on Codex CLI\n\nCodex CLI requires the Responses API (`/v1/responses`), which limits your options:\n\n- **Mantle:** Only the 4 OpenAI gpt-oss models support Responses API. Even with those, I hit 401 auth errors (Bearer token not passed correctly through Codex's HTTPS transport) and tool type rejections (`web_search` type not supported — only `function` and `mcp`).\n- **bedrock-access-gateway:** No Responses API at all — `/v1/responses` returns 404. The gateway only implements Chat Completions.\n- **LiteLLM:** Supports Responses API ([v1.66.3+](https://github.com/BerriAI/litellm/releases)) and has an [official Codex CLI tutorial](https://docs.litellm.ai/docs/tutorials/openai_codex). However, as of v1.82.5, there's a tool translation bug: Codex CLI sends built-in tool types that LiteLLM converts to Bedrock Converse format with empty `toolSpec.name` fields, causing Bedrock validation errors. The Responses API itself works fine when tested with standard function tools. This should be fixable on the LiteLLM side.\n\n## Quick setup: OpenCode + Mantle\n\nIf you just want to burn AWS credits on a coding CLI today, here's the fastest path. [OpenCode](https://opencode.ai) (v1.2.27+) works with Mantle out of the box:\n\n```json\n{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"provider\": {\n    \"bedrock-mantle\": {\n      \"npm\": \"@ai-sdk/openai-compatible\",\n      \"name\": \"Bedrock Mantle\",\n      \"options\": {\n        \"baseURL\": \"https://bedrock-mantle.us-east-1.api.aws/v1\",\n        \"apiKey\": \"{env:BEDROCK_API_KEY}\"\n      },\n      \"models\": {\n        \"openai.gpt-oss-120b\": { \"name\": \"GPT OSS 120B\" },\n        \"zai.glm-5\": { \"name\": \"GLM 5 (744B/40B MoE)\" },\n        \"qwen.qwen3-coder-480b-a35b-instruct\": { \"name\": \"Qwen3 Coder 480B\" },\n        \"deepseek.v3.2\": { \"name\": \"DeepSeek V3.2\" },\n        \"mistral.mistral-large-3-675b-instruct\": { \"name\": \"Mistral Large 3\" }\n      }\n    }\n  },\n  \"model\": \"bedrock-mantle/openai.gpt-oss-120b\"\n}\n```\n\nSave to `~/.config/opencode/opencode.json`, set `BEDROCK_API_KEY`, and you're coding.\n\n## The bottom line\n\n| | Mantle | bedrock-access-gateway | LiteLLM |\n|---|--------|----------------------|---------|\n| Infra to maintain | None | Lambda/ECS | Container/process |\n| Models available | 38 (open-weight) | All Bedrock | All Bedrock |\n| OpenAI Chat API | ✅ | ✅ | ✅ |\n| OpenAI Responses API | ⚠️ gpt-oss only | ❌ | ✅ |\n| Anthropic Messages API | ❌ | ❌ | ✅ |\n| OpenCode | ✅ | ✅ | ✅ |\n| Codex CLI | ❌ | ❌ | ⚠️ Tool bug |\n| Claude Code | ❌ | ❌ | ✅ |\n| Extra cost | None | ~$0 (Lambda) | Your compute |\n| Setup time | 5 min | 30 min | 1 hr |\n\n## Track available Mantle models\n\nI maintain [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io) — a catalog of every Bedrock model with API support badges and endpoint support (Mantle vs Runtime), scraped from the AWS documentation.\n\n---\n\n*Burning AWS credits on something interesting? I'd love to hear what tools and models you're using — drop a comment.*\n comment.*",
      "excerpts": [
        "You have AWS credits. You want to use them on AI coding tools — OpenCode, Codex CLI, Claude Code, whatever. Amazon Bedrock has the models. But how do you actually connect them?",
        "There are three approaches, and picking the wrong one wastes time. Here's the decision guide I wish I had.",
        "> All data in this post is as of March 2026. Model counts and API support may change — check [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io) for the latest.",
        "- **Just want it to work?** Mantle + OpenCode. Five minutes, zero infra. - **Need Claude models via OpenAI API?** bedrock-access-gateway on Lambda. - **Need Claude Code specifically?** LiteLLM. It's the only path. - **Codex CLI?** Broken with all three. Wait for LiteLLM to fix a tool translation bug.",
        "![Decision flowchart: Mantle vs bedrock-access-gateway vs LiteLLM](/assets/img/5ec03a820bf7.png)",
        "**One thing all three have in common: your API keys and code context stay within your AWS account or your own infrastructure.** Third-party AI gateways exist (Bifrost, Portkey, etc.), but they require routing your Bedrock API keys and code context through someone else's servers. Self-hosted or AWS-native — that's the baseline.",
        "1. Bedrock Mantle — no self-hosted infra required",
        "[Mantle](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) is AWS's native OpenAI-compatible endpoint. No Lambda, no container, no proxy — just set your base URL and API key:",
        "**What's on Mantle:** 38 open-weight models — DeepSeek, Mistral, Qwen, GLM, NVIDIA Nemotron, MiniMax, Moonshot Kimi, Google Gemma, OpenAI gpt-oss, and Writer Palmyra.",
        "**What's NOT on Mantle:** Anthropic Claude, Amazon Nova, Meta Llama, AI21, Cohere — the proprietary/first-party models are absent.",
        "**API coverage:** Mantle exposes Chat Completions (`/v1/chat/completions`) and Responses API (`/v1/responses`). No Anthropic Messages API (`/v1/messages`).",
        "The Responses API is limited — only 4 models support it: `openai.gpt-oss-120b-1:0`, `openai.gpt-oss-20b-1:0`, `openai.gpt-oss-120b`, and `openai.gpt-oss-20b`. Every other model is Chat Completions only. I verified this by scraping all 102 model card pages in the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html).",
        "**Cost:** Standard Bedrock on-demand pricing. No gateway markup, no infra costs.",
        "**Best for:** OpenCode or any tool that speaks OpenAI Chat Completions.",
        "2. bedrock-access-gateway — self-hosted, all models",
        "[bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway) (or my fork, [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url)) gives you an OpenAI-compatible proxy backed by all Bedrock models — including Claude, Nova, and Llama.",
        "Deploy it as a Lambda Function URL or on ECS, and you get:",
        "The tradeoff: you maintain infrastructure. But you get access to every Bedrock model through a single OpenAI-compatible endpoint.",
        "**Cost:** Bedrock on-demand pricing + Lambda/ECS compute costs (minimal for Lambda Function URLs — you pay per invocation).",
        "**Best for:** When you need Claude or Nova through OpenAI-compatible tools, or want full control over routing, caching, and logging.",
        "3. LiteLLM — the universal translator",
        "[LiteLLM](https://github.com/BerriAI/litellm) is the Swiss Army knife. It translates between API schemas — OpenAI, Anthropic, Bedrock native, and more. It's the only option that gives you Anthropic Messages API (`/v1/messages`) compatibility with Bedrock models.",
        "This matters because **Claude Code uses the Anthropic API schema**, not OpenAI's. If you want to run Claude Code against Bedrock, LiteLLM is your best (and arguably only) option. I tested this end-to-end: Claude Code CLI → LiteLLM → Bedrock Converse API — it works, including streaming responses.",
        "Is it perfect? No. Setup is more complex (Python process or Docker container, optional PostgreSQL for analytics), and you're adding another layer of abstraction. But it's the most flexible gateway available.",
        "**Cost:** Bedrock on-demand pricing + your compute costs for hosting LiteLLM. No per-call markup from LiteLLM itself (open source).",
        "**Best for:** Claude Code, or when you need both OpenAI and Anthropic API compatibility from a single proxy.",
        "| Tool | API Schema | Mantle | bedrock-access-gateway | LiteLLM | |------|-----------|--------|----------------------|---------| | OpenCode | OpenAI Chat | ✅ | ✅ | ✅ | | Codex CLI | OpenAI Responses | ❌ Auth issues | ❌ No Responses API | ⚠️ Tool bug | | Claude Code | Anthropic Messages | ❌ No support | ❌ Wrong schema | ✅ |",
        "Note: Anthropic-native tools like Kiro CLI also work through LiteLLM's Anthropic Messages API translation.",
        "Codex CLI requires the Responses API (`/v1/responses`), which limits your options:",
        "- **Mantle:** Only the 4 OpenAI gpt-oss models support Responses API. Even with those, I hit 401 auth errors (Bearer token not passed correctly through Codex's HTTPS transport) and tool type rejections (`web_search` type not supported — only `function` and `mcp`). - **bedrock-access-gateway:** No Responses API at all — `/v1/responses` returns 404. The gateway only implements Chat Completions. - **LiteLLM:** Supports Responses API ([v1.66.3+](https://github.com/BerriAI/litellm/releases)) and has an [official Codex CLI tutorial](https://docs.litellm.ai/docs/tutorials/openai_codex). However, as of v1.82.5, there's a tool translation bug: Codex CLI sends built-in tool types that LiteLLM converts to Bedrock Converse format with empty `toolSpec.name` fields, causing Bedrock validation errors. The Responses API itself works fine when tested with standard function tools. This should be fixable on the LiteLLM side.",
        "Quick setup: OpenCode + Mantle",
        "If you just want to burn AWS credits on a coding CLI today, here's the fastest path. [OpenCode](https://opencode.ai) (v1.2.27+) works with Mantle out of the box:",
        "Save to `~/.config/opencode/opencode.json`, set `BEDROCK_API_KEY`, and you're coding.",
        "| | Mantle | bedrock-access-gateway | LiteLLM | |---|--------|----------------------|---------| | Infra to maintain | None | Lambda/ECS | Container/process | | Models available | 38 (open-weight) | All Bedrock | All Bedrock | | OpenAI Chat API | ✅ | ✅ | ✅ | | OpenAI Responses API | ⚠️ gpt-oss only | ❌ | ✅ | | Anthropic Messages API | ❌ | ❌ | ✅ | | OpenCode | ✅ | ✅ | ✅ | | Codex CLI | ❌ | ❌ | ⚠️ Tool bug | | Claude Code | ❌ | ❌ | ✅ | | Extra cost | None | ~$0 (Lambda) | Your compute | | Setup time | 5 min | 30 min | 1 hr |",
        "I maintain [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io) — a catalog of every Bedrock model with API support badges and endpoint support (Mantle vs Runtime), scraped from the AWS documentation.",
        "*Burning AWS credits on something interesting? I'd love to hear what tools and models you're using — drop a comment.* comment.*"
      ]
    },
    {
      "id": "article:from-3-minute-cold-starts-to-20-seconds-whisper-on-aws-lambda-efs-for-openclaw-9c5",
      "source_type": "article",
      "title": "From 3-Minute Cold Starts to ~20 Seconds: Whisper on AWS Lambda + EFS for OpenClaw",
      "url": "https://gabrielkoo.com/blog/from-3-minute-cold-starts-to-20-seconds-whisper-on-aws-lambda-efs-for-openclaw-9c5/",
      "canonical_url": "https://dev.to/aws-builders/from-3-minute-cold-starts-to-20-seconds-whisper-on-aws-lambda-efs-for-openclaw-9c5",
      "published_at": "2026-03-13",
      "last_verified_at": "2026-08-23",
      "tags": [
        "whisper",
        "openclaw",
        "aws",
        "efs"
      ],
      "description": "Part 3 of my series on building a low-cost personal AI stack on AWS. Part 1 — Squeezing my $1k/month...",
      "content": "*Part 3 of my series on building a low-cost personal AI stack on AWS.* \n*[Part 1 — Squeezing my $1k/month API bill to $20/month with AWS Credits](https://dev.to/aws-builders/i-squeezed-my-1k-monthly-openclaw-api-bill-with-20month-in-aws-credits-heres-the-exact-setup-3gj4)*\n*[Part 2 — Drop-in Perplexity Sonar replacement with AWS Bedrock Nova Grounding](https://dev.to/aws-builders/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9)*\n\n---\n\n## TL;DR\n\nI built a self-hosted speech-to-text API on AWS Lambda using [faster-whisper](https://github.com/SYSTRAN/faster-whisper). After trying Amazon Transcribe, SageMaker Serverless, and Lambda with a bundled model, I landed on a **Lambda + EFS + S3** architecture that achieves ~20-30 second cold starts (once the model is cached on EFS) for ~$0.21/month in storage costs. Once warm, specifying the language drops response time to ~10s.\n\nOpen source: [gabrielkoo/aws-lambda-whisper-adaptor](https://github.com/gabrielkoo/aws-lambda-whisper-adaptor)\n\n---\n\n## The Problem\n\nI wanted to automatically transcribe Telegram voice messages. The requirements were simple:\n\n- **Accuracy**: Good enough for Cantonese\n- **Cost**: Pay-per-use, scales to zero when idle\n- **Latency**: Cold start under 60 seconds\n\nThere's a fourth constraint that's easy to overlook outside Hong Kong: **most managed STT APIs simply aren't available here**. OpenAI's Whisper API falls under their notorious China's regional restriction. Google's Gemini models are available and actually competitive on both accuracy and price — Gemini 3 Flash achieves 3.1% WER at ~$1.92/1000 minutes ([Artificial Analysis STT leaderboard](https://artificialanalysis.ai/speech-to-text)), cheaper than OpenAI's Whisper API and competitive with Lambda at low volume. The real reason I went with Lambda: AWS Credits from the Community Builder program (same theme as the rest of this series) make it effectively free.\n\nSimple enough. Except it took four attempts to get there.\n\n---\n\n## What I Tried (and Why It Didn't Work)\n\n### Option 1: Amazon Transcribe\n\nThe obvious first choice — fully managed, pay-per-use, native AWS integration.\n\n**Why I rejected it before even trying:**\n\nAmazon Transcribe [supports `zh-CN` and `zh-TW`, but not `yue` (Cantonese)](https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html). Whisper large-v3-turbo handles Cantonese significantly better, and accuracy matters more than convenience here.\n\n---\n\n### Option 2: SageMaker Serverless Inference\n\nSageMaker Serverless scales to zero and handles model serving — sounds perfect.\n\n**What happened:**\n\nI deployed a SageMaker Serverless endpoint with faster-whisper. The first invocation after idle:\n\n- Container provisioning: ~30s\n- Model loading: ~45-60s\n- **Total cold start: 60-90 seconds**\n\nFor a voice message that's 5-10 seconds long, waiting 90 seconds is a terrible experience.\n\n**The 6GB memory wall:**\n\nSageMaker Serverless [maxes out at 6144 MB (6 GB) RAM](https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints.html). Here's why that's a problem for Whisper:\n\n- [`whisper-large-v3-turbo` (INT8)](https://huggingface.co/Zoont/faster-whisper-large-v3-turbo-int8-ct2): ~780MB model + ~2GB Python/runtime overhead ≈ 2.8GB minimum\n- [`whisper-large-v3` (FP16)](https://huggingface.co/Systran/faster-whisper-large-v3): ~3GB model alone — barely fits, zero headroom for audio processing\n- Any concurrent requests? You're OOM.\n\n[Lambda goes up to 10,240 MB](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html). That headroom matters.\n\n**Cost comparison:**\n\n[SageMaker Serverless bills per GB-second](https://aws.amazon.com/sagemaker/pricing/) of inference time. For sporadic voice message transcription (~10s per request, a few times a day), Lambda's per-invocation pricing is significantly cheaper. My Lambda setup costs ~$0.21/month in storage — the compute is essentially free at this volume.\n\nI deleted the endpoint after testing.\n\n---\n\n### Option 2b: Bedrock Marketplace\n\nAWS Bedrock Marketplace [does list Whisper Large V3 Turbo](https://aws.amazon.com/blogs/machine-learning/build-a-serverless-audio-summarization-solution-with-amazon-bedrock-and-whisper/) — but it deploys on a **dedicated endpoint instance**. Auto-scaling is available (including scale-to-zero), but that creates a different problem:\n\n- **Keep minimum 1 instance**: always paying for idle time, even at 3am\n- **Scale to zero**: cold starts when traffic resumes — SageMaker cold starts are measured in **minutes**, not seconds\n- Not token/usage-based pricing either way\n\nFor a Telegram bot that gets a few voice messages a day, you're either burning money on idle instances or waiting minutes for the first message to transcribe. Lambda's ~20-30s cold start looks great by comparison.\n\n---\n\n### Option 3: Lambda with Bundled Model\n\nNext idea: bundle the model directly into the Docker image. No external dependencies, simple architecture.\n\n**What happened:**\n\n```dockerfile\n# Download model during build\n# Note: using openai/whisper-large-v3-turbo converted to int8 via sync-model workflow\nRUN python -c \"from faster_whisper import WhisperModel; WhisperModel('openai/whisper-large-v3-turbo')\"\n```\n\n- Docker image size: **~10GB**\n- ECR push time: **5+ minutes**\n- Lambda cold start: **2 minutes 51 seconds**\n\nThe cold start is dominated by Lambda pulling the 10GB image from ECR. AWS Lambda caches images, but any cold start after the cache expires hits this wall.\n\n**Why it didn't work:**\n\n- 3-minute cold start is unusable for interactive transcription\n- Every code change requires rebuilding and pushing a 10GB image\n- ECR storage: ~$1/month just for the image\n\n---\n\n### Option 4: Lambda + S3 (No EFS)\n\nWhat if Lambda downloads the model from S3 on cold start, storing it in `/tmp`?\n\n**The problem:**\n\nLambda's `/tmp` is ephemeral. Every cold start re-downloads the model from S3:\n\n- S3 download for 1.6GB FP16 model: **30-60 seconds**\n- S3 download for 780MB INT8 model: **15-30 seconds**\n\nThis is better than the bundled model approach, but there's a bigger issue: **no caching between Lambda instances**. If you have 3 concurrent invocations, all 3 download the model independently. You're paying for S3 transfer on every cold start.\n\n**What about Lambda SnapStart or Durable Functions?**\n\nAWS added two relevant features since this was written:\n\n- **SnapStart for Python** (Nov 2024): snapshots the initialized execution environment — sounds perfect for caching a loaded model. The catch: [SnapStart doesn't support container images](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html). This adaptor is container-based, so it's off the table.\n\n- **[Lambda Durable Functions](https://aws.amazon.com/about-aws/whats-new/2025/12/lambda-durable-multi-step-applications-ai-workflows/)** (re:Invent 2025): enables multi-step workflows with automatic checkpointing, pause/resume for up to one year, and failure recovery. This is workflow orchestration (think Azure Durable Functions) — useful for multi-step AI pipelines, but not for persisting a 780MB model binary between cold starts.\n\nEFS remains the right solution for model caching.\n\n---\n\n## What Actually Worked: Lambda + EFS + S3\n\nThe solution: use **EFS as a persistent model cache**, bootstrapped from S3. I've used EFS for [persistent Streamlit state on ECS](https://dev.to/aws-builders/scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm) before — same pattern, different compute layer.\n\n```plaintext\nRequest → Lambda Function URL\n               ↓\n          Lambda (VPC)\n               ↓ first cold start only: S3 → EFS\n              EFS (model cached here permanently)\n```\n\n![Logic Flow](/assets/img/57f530f3907a.png)\n\n\n\n\n**How it works:**\n\n1. **First cold start** (once per model): Lambda checks for a marker file on EFS. If missing, downloads model from S3 to EFS (~55s for INT8). Writes marker file. Then loads model into RAM.\n2. **Subsequent cold starts** (new container, model already on EFS): Marker file exists → load model from EFS into RAM (~20-30s for INT8).\n3. **Warm invocations** (same container reused): Model already in memory → transcription-only time (~10-22s depending on audio length and whether language is specified).\n\n```python\nHF_MODEL_REPO = os.environ.get('HF_MODEL_REPO', 'openai/whisper-large-v3-turbo')\nMODEL_SLUG = HF_MODEL_REPO.replace('/', '--')\nEFS_MODEL_DIR = f'/mnt/whisper-models/{MODEL_SLUG}'\nMODEL_MARKER = f'/mnt/whisper-models/.ready-{MODEL_SLUG}'\n\ndef bootstrap_model():\n    if os.path.exists(MODEL_MARKER):\n        return WhisperModel(EFS_MODEL_DIR, device='cpu', compute_type='int8')\n    \n    # First run: sync model from S3 to EFS\n    s3 = boto3.client('s3')\n    prefix = f'models/{MODEL_SLUG}/'\n    os.makedirs(EFS_MODEL_DIR, exist_ok=True)\n    \n    paginator = s3.get_paginator('list_objects_v2')\n    for page in paginator.paginate(Bucket=os.environ['MODEL_S3_BUCKET'], Prefix=prefix):\n        for obj in page.get('Contents', []):\n            key = obj['Key']\n            local_path = os.path.join(EFS_MODEL_DIR, key[len(prefix):])\n            os.makedirs(os.path.dirname(local_path), exist_ok=True)\n            s3.download_file(os.environ['MODEL_S3_BUCKET'], key, local_path)\n    \n    open(MODEL_MARKER, 'w').close()  # Mark as ready\n    return WhisperModel(EFS_MODEL_DIR, device='cpu', compute_type='int8')\n\nMODEL = bootstrap_model()  # Runs at Lambda init time, cached for warm invocations\n```\n\n**Why EFS works:**\n\n- EFS persists across Lambda instances — model is downloaded **once**, reused forever\n- EFS is mounted at `/mnt/whisper-models` — Lambda reads it like a local filesystem\n- **S3 VPC Gateway Endpoint is free** — no NAT Gateway needed (saves ~$32/month)\n- **Zero internet egress** — Lambda → S3 via VPC Gateway Endpoint, Lambda → EFS within VPC. The Lambda function never reaches the internet. This is a meaningful security benefit when using third-party models from HuggingFace — model weights never leave the AWS network once synced to S3.\n- EFS storage: ~$0.19/month for the 780MB INT8 model\n\n> 🔒 **Security note:** The Lambda runs in a VPC with **no internet access** — no NAT Gateway, no public subnet. It can only reach EFS (VPC-internal) and S3 (via the free VPC Gateway Endpoint). This means even if you're using a third-party HuggingFace model, the model weights and your audio data never leave the AWS network. No data exfiltration risk, no outbound calls to unknown endpoints.\n\n---\n\n## INT8 vs FP16: The Model Size Trade-off\n\nThe `openai/whisper-large-v3-turbo` model on HuggingFace needs conversion to CTranslate2 format. The `sync-model` workflow handles this, converting to INT8 and fixing the `num_mel_bins` config. Alternatively, use [`Zoont/faster-whisper-large-v3-turbo-int8-ct2`](https://huggingface.co/Zoont/faster-whisper-large-v3-turbo-int8-ct2) — a pre-converted CTranslate2 INT8 model that works out of the box with `quantization=none`:\n\n| Model | Size (EFS) | First Bootstrap | EFS Cold Start | Warm (2.5s audio) | Memory |\n|-------|-----------|-----------------|----------------|-------------------|--------|\n| `Zoont/faster-whisper-large-v3-turbo-int8-ct2` | ~780MB | ~55s | **~22s** ✅ | **~10s** ✅ | ~2.8GB |\n| `openai/whisper-large-v3-turbo` (INT8, via sync-model) | ~780MB | ~55s | ~22s | ~10s | ~2.8GB |\n| `openai/whisper-large-v3-turbo` (FP16) | ~1.5GB | ~126s | ~40s | ~15s | ~4GB |\n| `Systran/faster-whisper-large-v3` (FP16, loaded as int8) | ~1.6GB | ~54s | ~30s | ~13s | 6GB |\n\n**Recommended:** `Zoont/faster-whisper-large-v3-turbo-int8-ct2` — no conversion step needed, identical performance to the openai model converted to INT8. Use `quantization=none` in the sync-model workflow since it's already in CTranslate2 format.\n\n---\n\n## Cost Breakdown\n\n| Resource | Monthly Cost |\n|----------|-------------|\n| EFS storage (780MB INT8) | ~$0.19 |\n| S3 storage (780MB) | ~$0.02 |\n| Lambda compute | ~$0.00167/warm invocation* |\n| S3 VPC Gateway Endpoint | **Free** |\n| NAT Gateway | **Not needed ($0)** |\n| **Total (storage only)** | **~$0.21/month** |\n\n*10GB × 10s = 100 GB-seconds per warm invocation. The [Lambda free tier](https://aws.amazon.com/lambda/pricing/) covers **400,000 GB-seconds/month** — roughly 4,000 warm invocations. For a personal bot, compute cost is effectively **$0**. Storage dominates.\n\nCompare to SageMaker Serverless: minimum ~$5-10/month for similar workloads, plus the 60-90s cold start penalty.\n\n> **Why not Provisioned Concurrency?** PC keeps Lambda permanently warm (no cold starts), but costs ~$0.0000097222/GB-second. For a 10GB function running 24/7: ~$252/month. Even a minimal 4GB setup runs ~$100/month — roughly 500x more than the $0.21 storage approach. For a personal bot with a few voice messages a day, the occasional ~60s cold start is a fine trade-off.\n\n### vs. OpenAI Whisper API\n\nOpenAI's Whisper API costs [**$0.006/minute**](https://openai.com/api/pricing/). Here's how it compares for a bot averaging 15s voice messages:\n\n| Volume | OpenAI Whisper API | Self-hosted Lambda |\n|--------|-------------------|-------------------|\n| 50 msgs/month | $0.08 | $0.21 (storage only) |\n| 140 msgs/month | $0.21 | **$0.21** ← break-even |\n| 500 msgs/month | $0.75 | $0.21 (storage only) |\n| 1,000 msgs/month | $1.50 | $0.21 (storage only) |\n| 4,000 msgs/month | $6.00 | $0.21 (storage only) |\n\nLambda compute is free within the free tier (~4,000 warm invocations/month). Beyond that, it's $0.00167/invocation — but that's a high volume for a personal bot.\n\nBreak-even: **~140 messages/month**. Above that, Lambda wins on cost.\n\nBut cost isn't the only reason to self-host:\n- **Geographic availability**: OpenAI's API is not available in Hong Kong — HK falls under China's regional restriction. Azure OpenAI does offer Whisper, but [only `whisper-1` (large-v2 based)](https://learn.microsoft.com/en-us/answers/questions/2237575/new-announced-speech-to-text-models-for-realtime) — large-v3 and large-v3-turbo are not available. If you're in HK (or other restricted regions), this approach isn't just cheaper — it's the only option for v3-quality transcription.\n- **Cantonese accuracy**: `language=yue` with Whisper large-v3-turbo is noticeably better than the managed API for Cantonese\n- **Privacy**: audio never leaves your infrastructure\n- **No rate limits**: Lambda scales independently\n\n---\n\n## Architecture\n\n```plaintext\nTelegram voice message\n        ↓\n   OpenClaw (gateway)\n        ↓\nLambda Function URL (auth via token)\n        ↓\nLambda (VPC, 10GB RAM, 900s timeout)\n        ↓\nEFS /mnt/whisper-models/{model-slug}\n        ↓\nfaster-whisper (CTranslate2, INT8)\n        ↓\n    Transcript\n```\n\n**Lambda configuration:**\n- Memory: 10,240 MB — actual usage is **~2.2GB** (INT8 model), but [Lambda allocates CPU proportional to memory](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html). 10GB gives ~6 vCPUs vs ~2.3 vCPUs at 4GB, cutting warm transcription from ~16s to ~10s. You're paying for CPU, not RAM.\n- Timeout: 900s (handles long audio files)\n- VPC: Default VPC (no NAT Gateway)\n- EFS: Mounted at `/mnt/whisper-models`\n\n**Memory vs. cost trade-off (tested, 3 runs each):**\n\n| Config | Cold Start | Warm (2.5s audio) | GB-seconds/invocation |\n|--------|-----------|-------------------|----------------------|\n| 4,096 MB | ~30s | ~21s | 84 (~$0.00140) |\n| 6,144 MB | ~25s | ~16s | 96 (~$0.00160) |\n| 8,192 MB | ~24s | ~18s | 144 (~$0.00240) |\n| 10,240 MB | ~22s | ~15s | 150 (~$0.00250) |\n\nCold start is ~20-30s across all configs — it's EFS I/O bound, not CPU bound, so more memory doesn't help much here. Warm inference time does scale with memory (more vCPUs = faster CTranslate2 decoding). Interestingly, 4GB is the cheapest per invocation — the warm time savings at higher memory don't offset the extra GB-seconds. Within the free tier, cost differences are negligible regardless.\n\n---\n\n## API Compatibility\n\nThe adaptor exposes two endpoints so it works as a drop-in replacement for existing integrations:\n\n**OpenAI compatible** (`/v1/audio/transcriptions`):\n```bash\ncurl -X POST https://<function-url>/v1/audio/transcriptions \\\n  -H \"Authorization: Token <secret>\" \\\n  -F \"file=@audio.ogg\" \\\n  -F \"language=yue\"\n```\n```json\n{\"text\": \"transcript here\"}\n```\n\n**Deepgram compatible** (`/v1/listen`):\n```bash\ncurl -X POST https://<function-url>/v1/listen?language=yue \\\n  -H \"Authorization: Token <secret>\" \\\n  -H \"Content-Type: audio/ogg\" \\\n  --data-binary @audio.ogg\n```\n\n---\n\n## Model Management API\n\nOnce you've synced multiple models to EFS, there's no SSH access to see what's there or clean up. I added two non-standard endpoints:\n\n**List models on EFS:**\n```bash\ncurl https://<function-url>/v1/models -H \"Authorization: Token <secret>\"\n```\n```json\n{\n  \"object\": \"list\",\n  \"data\": [\n    {\"id\": \"openai/whisper-large-v3-turbo\", \"object\": \"model\", \"owned_by\": \"openai\"},\n    {\"id\": \"Systran/faster-distil-whisper-large-v3\", \"object\": \"model\", \"owned_by\": \"Systran\"}\n  ]\n}\n```\n\n**Delete a model from EFS** (the currently loaded model returns 409):\n```bash\ncurl -X DELETE https://<function-url>/v1/models/Systran/faster-distil-whisper-large-v3 \\\n  -H \"Authorization: Token <secret>\"\n```\n```json\n{\"id\": \"Systran/faster-distil-whisper-large-v3\", \"object\": \"model\", \"deleted\": true}\n```\n\nSlashes in model IDs work naturally — `rawPath` preserves the full path, so `DELETE /v1/models/openai/whisper-large-v3-turbo` correctly maps to model ID `openai/whisper-large-v3-turbo`.\n\n---\n\n## Performance Tip: Always Specify Language\n\nWhen no language is specified, Whisper runs language detection on the first audio chunk — adding noticeable overhead. For a 2.5s voice message:\n\n| Request | Response Time |\n|---------|--------------|\n| No language (auto-detect) | ~22s |\n| `language=yue` (Cantonese) | ~10s |\n\nThat's a **2x speedup** just from passing a language hint. Two ways to do it:\n\n**Option A — per-request query param** (recommended, keeps Lambda language-agnostic):\n```bash\n# Deepgram endpoint\ncurl -X POST https://<function-url>/v1/listen?language=yue \\\n  -H \"Authorization: Token <secret>\" \\\n  -H \"Content-Type: audio/ogg\" \\\n  --data-binary @audio.ogg\n\n# OpenAI endpoint\ncurl -X POST https://<function-url>/v1/audio/transcriptions \\\n  -H \"Authorization: Token <secret>\" \\\n  -F \"file=@audio.ogg\" \\\n  -F \"language=yue\"\n```\n\n**Option B — Lambda env var** (simpler if you only ever transcribe one language):\n```bash\nWHISPER_LANGUAGE=yue\n```\n\nI use Option A — the language is set in my OpenClaw config (`language: \"yue\"` in the audio model), which passes it as `?language=yue` to the Lambda on every request.\n\n### Real-time Factor\n\nOnce warm, the Lambda transcribes faster than real-time for typical voice messages:\n\n| Audio Duration | Warm Response Time | Real-time Factor |\n|---------------|-------------------|-----------------|\n| 2.5s | ~10s | 4x |\n| 33s | ~23s | **0.68x** ✅ faster than real-time |\n\nThe 2.5s result looks slow (4x), but Whisper processes audio in 30-second chunks — the overhead is fixed regardless of audio length. For longer messages, the real-time factor drops well below 1x.\n\n---\n\n## Open Source\n\nThe project is open source at [gabrielkoo/aws-lambda-whisper-adaptor](https://github.com/gabrielkoo/aws-lambda-whisper-adaptor).\n\nKey features:\n- Any [faster-whisper](https://huggingface.co/models?search=faster-whisper) model via `HF_MODEL_REPO` env var\n- GitHub Actions workflow to sync models from HuggingFace → S3 (`quantization=int8` for HF-format models, `quantization=none` for pre-converted CTranslate2 models)\n- `GET /v1/models` — list all models currently on EFS\n- `DELETE /v1/models/{owner}/{model}` — remove a model from EFS on demand\n- Pre-built Docker image: `ghcr.io/gabrielkoo/aws-lambda-whisper-adaptor:latest`\n- Configurable language detection via `WHISPER_LANGUAGE` env var or per-request parameter\n\n---\n\n## Pre-warming\n\n> **For OpenClaw voice prompts:** the ~20-30s cold start is often negligible in practice — if you're asking the agent to run a multi-step job, it'll take a few minutes anyway. Pre-warming only matters if you need the very first transcription to be fast.\n\nCold starts happen when Lambda hasn't been invoked recently. For predictable usage patterns (e.g. a morning standup bot), pre-warm the Lambda before you need it:\n\n```bash\n#!/bin/bash\n# prewarm.sh — trigger Lambda init before expected usage\ncurl -s -o /dev/null \\\n  -X POST \"$WHISPER_LAMBDA_URL/v1/listen?language=yue\" \\\n  -H \"Authorization: Token $WHISPER_API_SECRET\" \\\n  -H \"Content-Type: audio/ogg\" \\\n  --data-binary @sample.ogg\necho \"Lambda pre-warmed\"\n```\n\nSchedule with cron: `0 8 * * * /path/to/prewarm.sh` (runs at 8am daily).\n\nAlternatively, use an EventBridge rule to ping the Lambda every few minutes — though at that frequency, Provisioned Concurrency starts making more sense cost-wise.\n\n---\n\n## Conclusion\n\nThe Lambda + EFS + S3 architecture achieves:\n- **~20-30s cold start** (INT8 model on EFS); first-ever bootstrap from S3 takes ~55s (one-time only)\n- **~10s warm invocations** with `language=yue`\n- **~$0.21/month** storage cost\n- **Zero idle cost** (scales to zero)\n- **Deepgram and OpenAI compatible** APIs\n\nThe key insight: **EFS is the missing piece**. It provides persistent, fast storage that Lambda can access without a NAT Gateway (using the free S3 VPC Gateway Endpoint for bootstrapping).\n\nI couldn't find any existing write-up of Whisper on Lambda using EFS for persistent model caching — most approaches either bundle the model in Docker (3-minute cold starts) or re-download from S3 on every cold start (no caching between instances). If you've seen this done before, I'd love to know.\n\nTwo things worth knowing before you deploy:\n1. Use `Zoont/faster-whisper-large-v3-turbo-int8-ct2` with `quantization=none` in the sync-model workflow — it's pre-converted to CTranslate2 INT8 and works out of the box (the `openai/whisper-large-v3-turbo` model requires conversion and can hit `num_mel_bins` config issues)\n2. Always pass a `language` parameter if you know it — cuts response time roughly in half\n\nIf you're building voice transcription on AWS and want Whisper-quality accuracy without the SageMaker complexity, give it a try.\n\n---\n\n*Using EFS as a persistent model cache follows the same pattern I used earlier for [scaling a stateful Streamlit chatbot with ECS + EFS](https://dev.to/aws-builders/scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm) — if you're building other stateful workloads on AWS, that one's worth a look too.*",
      "excerpts": [
        "*Part 3 of my series on building a low-cost personal AI stack on AWS.* *[Part 1 — Squeezing my $1k/month API bill to $20/month with AWS Credits](https://dev.to/aws-builders/i-squeezed-my-1k-monthly-openclaw-api-bill-with-20month-in-aws-credits-heres-the-exact-setup-3gj4)* *[Part 2 — Drop-in Perplexity Sonar replacement with AWS Bedrock Nova Grounding](https://dev.to/aws-builders/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9)*",
        "I built a self-hosted speech-to-text API on AWS Lambda using [faster-whisper](https://github.com/SYSTRAN/faster-whisper). After trying Amazon Transcribe, SageMaker Serverless, and Lambda with a bundled model, I landed on a **Lambda + EFS + S3** architecture that achieves ~20-30 second cold starts (once the model is cached on EFS) for ~$0.21/month in storage costs. Once warm, specifying the language drops response time to ~10s.",
        "Open source: [gabrielkoo/aws-lambda-whisper-adaptor](https://github.com/gabrielkoo/aws-lambda-whisper-adaptor)",
        "I wanted to automatically transcribe Telegram voice messages. The requirements were simple:",
        "- **Accuracy**: Good enough for Cantonese - **Cost**: Pay-per-use, scales to zero when idle - **Latency**: Cold start under 60 seconds",
        "There's a fourth constraint that's easy to overlook outside Hong Kong: **most managed STT APIs simply aren't available here**. OpenAI's Whisper API falls under their notorious China's regional restriction. Google's Gemini models are available and actually competitive on both accuracy and price — Gemini 3 Flash achieves 3.1% WER at ~$1.92/1000 minutes ([Artificial Analysis STT leaderboard](https://artificialanalysis.ai/speech-to-text)), cheaper than OpenAI's Whisper API and competitive with Lambda at low volume. The real reason I went with Lambda: AWS Credits from the Community Builder program (same theme as the rest of this series) make it effectively free.",
        "Simple enough. Except it took four attempts to get there.",
        "What I Tried (and Why It Didn't Work)",
        "The obvious first choice — fully managed, pay-per-use, native AWS integration.",
        "**Why I rejected it before even trying:**",
        "Amazon Transcribe [supports `zh-CN` and `zh-TW`, but not `yue` (Cantonese)](https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html). Whisper large-v3-turbo handles Cantonese significantly better, and accuracy matters more than convenience here.",
        "Option 2: SageMaker Serverless Inference",
        "SageMaker Serverless scales to zero and handles model serving — sounds perfect.",
        "I deployed a SageMaker Serverless endpoint with faster-whisper. The first invocation after idle:",
        "- Container provisioning: ~30s - Model loading: ~45-60s - **Total cold start: 60-90 seconds**",
        "For a voice message that's 5-10 seconds long, waiting 90 seconds is a terrible experience.",
        "SageMaker Serverless [maxes out at 6144 MB (6 GB) RAM](https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints.html). Here's why that's a problem for Whisper:",
        "- [`whisper-large-v3-turbo` (INT8)](https://huggingface.co/Zoont/faster-whisper-large-v3-turbo-int8-ct2): ~780MB model + ~2GB Python/runtime overhead ≈ 2.8GB minimum - [`whisper-large-v3` (FP16)](https://huggingface.co/Systran/faster-whisper-large-v3): ~3GB model alone — barely fits, zero headroom for audio processing - Any concurrent requests? You're OOM.",
        "[Lambda goes up to 10,240 MB](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html). That headroom matters.",
        "[SageMaker Serverless bills per GB-second](https://aws.amazon.com/sagemaker/pricing/) of inference time. For sporadic voice message transcription (~10s per request, a few times a day), Lambda's per-invocation pricing is significantly cheaper. My Lambda setup costs ~$0.21/month in storage — the compute is essentially free at this volume.",
        "I deleted the endpoint after testing.",
        "Option 2b: Bedrock Marketplace",
        "AWS Bedrock Marketplace [does list Whisper Large V3 Turbo](https://aws.amazon.com/blogs/machine-learning/build-a-serverless-audio-summarization-solution-with-amazon-bedrock-and-whisper/) — but it deploys on a **dedicated endpoint instance**. Auto-scaling is available (including scale-to-zero), but that creates a different problem:",
        "- **Keep minimum 1 instance**: always paying for idle time, even at 3am - **Scale to zero**: cold starts when traffic resumes — SageMaker cold starts are measured in **minutes**, not seconds - Not token/usage-based pricing either way",
        "For a Telegram bot that gets a few voice messages a day, you're either burning money on idle instances or waiting minutes for the first message to transcribe. Lambda's ~20-30s cold start looks great by comparison.",
        "Option 3: Lambda with Bundled Model",
        "Next idea: bundle the model directly into the Docker image. No external dependencies, simple architecture.",
        "- Docker image size: **~10GB** - ECR push time: **5+ minutes** - Lambda cold start: **2 minutes 51 seconds**",
        "The cold start is dominated by Lambda pulling the 10GB image from ECR. AWS Lambda caches images, but any cold start after the cache expires hits this wall.",
        "- 3-minute cold start is unusable for interactive transcription - Every code change requires rebuilding and pushing a 10GB image - ECR storage: ~$1/month just for the image",
        "Option 4: Lambda + S3 (No EFS)",
        "What if Lambda downloads the model from S3 on cold start, storing it in `/tmp`?",
        "Lambda's `/tmp` is ephemeral. Every cold start re-downloads the model from S3:",
        "- S3 download for 1.6GB FP16 model: **30-60 seconds** - S3 download for 780MB INT8 model: **15-30 seconds**",
        "This is better than the bundled model approach, but there's a bigger issue: **no caching between Lambda instances**. If you have 3 concurrent invocations, all 3 download the model independently. You're paying for S3 transfer on every cold start.",
        "**What about Lambda SnapStart or Durable Functions?**",
        "AWS added two relevant features since this was written:",
        "- **SnapStart for Python** (Nov 2024): snapshots the initialized execution environment — sounds perfect for caching a loaded model. The catch: [SnapStart doesn't support container images](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html). This adaptor is container-based, so it's off the table.",
        "- **[Lambda Durable Functions](https://aws.amazon.com/about-aws/whats-new/2025/12/lambda-durable-multi-step-applications-ai-workflows/)** (re:Invent 2025): enables multi-step workflows with automatic checkpointing, pause/resume for up to one year, and failure recovery. This is workflow orchestration (think Azure Durable Functions) — useful for multi-step AI pipelines, but not for persisting a 780MB model binary between cold starts.",
        "EFS remains the right solution for model caching.",
        "What Actually Worked: Lambda + EFS + S3",
        "The solution: use **EFS as a persistent model cache**, bootstrapped from S3. I've used EFS for [persistent Streamlit state on ECS](https://dev.to/aws-builders/scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm) before — same pattern, different compute layer.",
        "![Logic Flow](/assets/img/57f530f3907a.png)",
        "1. **First cold start** (once per model): Lambda checks for a marker file on EFS. If missing, downloads model from S3 to EFS (~55s for INT8). Writes marker file. Then loads model into RAM. 2. **Subsequent cold starts** (new container, model already on EFS): Marker file exists → load model from EFS into RAM (~20-30s for INT8). 3. **Warm invocations** (same container reused): Model already in memory → transcription-only time (~10-22s depending on audio length and whether language is specified).",
        "def bootstrap_model(): if os.path.exists(MODEL_MARKER): return WhisperModel(EFS_MODEL_DIR, device='cpu', compute_type='int8') # First run: sync model from S3 to EFS s3 = boto3.client('s3') prefix = f'models/{MODEL_SLUG}/' os.makedirs(EFS_MODEL_DIR, exist_ok=True) paginator = s3.get_paginator('list_objects_v2') for page in paginator.paginate(Bucket=os.environ['MODEL_S3_BUCKET'], Prefix=prefix): for obj in page.get('Contents', []): key = obj['Key'] local_path = os.path.join(EFS_MODEL_DIR, key[len(prefix):]) os.makedirs(os.path.dirname(local_path), exist_ok=True) s3.download_file(os.environ['MODEL_S3_BUCKET'], key, local_path) open(MODEL_MARKER, 'w').close() # Mark as ready return WhisperModel(EFS_MODEL_DIR, device='cpu', compute_type='int8')",
        "MODEL = bootstrap_model() # Runs at Lambda init time, cached for warm invocations ```",
        "- EFS persists across Lambda instances — model is downloaded **once**, reused forever - EFS is mounted at `/mnt/whisper-models` — Lambda reads it like a local filesystem - **S3 VPC Gateway Endpoint is free** — no NAT Gateway needed (saves ~$32/month) - **Zero internet egress** — Lambda → S3 via VPC Gateway Endpoint, Lambda → EFS within VPC. The Lambda function never reaches the internet. This is a meaningful security benefit when using third-party models from HuggingFace — model weights never leave the AWS network once synced to S3. - EFS storage: ~$0.19/month for the 780MB INT8 model",
        "> 🔒 **Security note:** The Lambda runs in a VPC with **no internet access** — no NAT Gateway, no public subnet. It can only reach EFS (VPC-internal) and S3 (via the free VPC Gateway Endpoint). This means even if you're using a third-party HuggingFace model, the model weights and your audio data never leave the AWS network. No data exfiltration risk, no outbound calls to unknown endpoints.",
        "INT8 vs FP16: The Model Size Trade-off",
        "The `openai/whisper-large-v3-turbo` model on HuggingFace needs conversion to CTranslate2 format. The `sync-model` workflow handles this, converting to INT8 and fixing the `num_mel_bins` config. Alternatively, use [`Zoont/faster-whisper-large-v3-turbo-int8-ct2`](https://huggingface.co/Zoont/faster-whisper-large-v3-turbo-int8-ct2) — a pre-converted CTranslate2 INT8 model that works out of the box with `quantization=none`:",
        "| Model | Size (EFS) | First Bootstrap | EFS Cold Start | Warm (2.5s audio) | Memory | |-------|-----------|-----------------|----------------|-------------------|--------| | `Zoont/faster-whisper-large-v3-turbo-int8-ct2` | ~780MB | ~55s | **~22s** ✅ | **~10s** ✅ | ~2.8GB | | `openai/whisper-large-v3-turbo` (INT8, via sync-model) | ~780MB | ~55s | ~22s | ~10s | ~2.8GB | | `openai/whisper-large-v3-turbo` (FP16) | ~1.5GB | ~126s | ~40s | ~15s | ~4GB | | `Systran/faster-whisper-large-v3` (FP16, loaded as int8) | ~1.6GB | ~54s | ~30s | ~13s | 6GB |",
        "**Recommended:** `Zoont/faster-whisper-large-v3-turbo-int8-ct2` — no conversion step needed, identical performance to the openai model converted to INT8. Use `quantization=none` in the sync-model workflow since it's already in CTranslate2 format.",
        "| Resource | Monthly Cost | |----------|-------------| | EFS storage (780MB INT8) | ~$0.19 | | S3 storage (780MB) | ~$0.02 | | Lambda compute | ~$0.00167/warm invocation* | | S3 VPC Gateway Endpoint | **Free** | | NAT Gateway | **Not needed ($0)** | | **Total (storage only)** | **~$0.21/month** |",
        "*10GB × 10s = 100 GB-seconds per warm invocation. The [Lambda free tier](https://aws.amazon.com/lambda/pricing/) covers **400,000 GB-seconds/month** — roughly 4,000 warm invocations. For a personal bot, compute cost is effectively **$0**. Storage dominates.",
        "Compare to SageMaker Serverless: minimum ~$5-10/month for similar workloads, plus the 60-90s cold start penalty.",
        "> **Why not Provisioned Concurrency?** PC keeps Lambda permanently warm (no cold starts), but costs ~$0.0000097222/GB-second. For a 10GB function running 24/7: ~$252/month. Even a minimal 4GB setup runs ~$100/month — roughly 500x more than the $0.21 storage approach. For a personal bot with a few voice messages a day, the occasional ~60s cold start is a fine trade-off.",
        "OpenAI's Whisper API costs [**$0.006/minute**](https://openai.com/api/pricing/). Here's how it compares for a bot averaging 15s voice messages:",
        "| Volume | OpenAI Whisper API | Self-hosted Lambda | |--------|-------------------|-------------------| | 50 msgs/month | $0.08 | $0.21 (storage only) | | 140 msgs/month | $0.21 | **$0.21** ← break-even | | 500 msgs/month | $0.75 | $0.21 (storage only) | | 1,000 msgs/month | $1.50 | $0.21 (storage only) | | 4,000 msgs/month | $6.00 | $0.21 (storage only) |",
        "Lambda compute is free within the free tier (~4,000 warm invocations/month). Beyond that, it's $0.00167/invocation — but that's a high volume for a personal bot.",
        "Break-even: **~140 messages/month**. Above that, Lambda wins on cost.",
        "But cost isn't the only reason to self-host: - **Geographic availability**: OpenAI's API is not available in Hong Kong — HK falls under China's regional restriction. Azure OpenAI does offer Whisper, but [only `whisper-1` (large-v2 based)](https://learn.microsoft.com/en-us/answers/questions/2237575/new-announced-speech-to-text-models-for-realtime) — large-v3 and large-v3-turbo are not available. If you're in HK (or other restricted regions), this approach isn't just cheaper — it's the only option for v3-quality transcription. - **Cantonese accuracy**: `language=yue` with Whisper large-v3-turbo is noticeably better than the managed API for Cantonese - **Privacy**: audio never leaves your infrastructure - **No rate limits**: Lambda scales independently",
        "**Lambda configuration:** - Memory: 10,240 MB — actual usage is **~2.2GB** (INT8 model), but [Lambda allocates CPU proportional to memory](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html). 10GB gives ~6 vCPUs vs ~2.3 vCPUs at 4GB, cutting warm transcription from ~16s to ~10s. You're paying for CPU, not RAM. - Timeout: 900s (handles long audio files) - VPC: Default VPC (no NAT Gateway) - EFS: Mounted at `/mnt/whisper-models`",
        "**Memory vs. cost trade-off (tested, 3 runs each):**",
        "| Config | Cold Start | Warm (2.5s audio) | GB-seconds/invocation | |--------|-----------|-------------------|----------------------| | 4,096 MB | ~30s | ~21s | 84 (~$0.00140) | | 6,144 MB | ~25s | ~16s | 96 (~$0.00160) | | 8,192 MB | ~24s | ~18s | 144 (~$0.00240) | | 10,240 MB | ~22s | ~15s | 150 (~$0.00250) |",
        "Cold start is ~20-30s across all configs — it's EFS I/O bound, not CPU bound, so more memory doesn't help much here. Warm inference time does scale with memory (more vCPUs = faster CTranslate2 decoding). Interestingly, 4GB is the cheapest per invocation — the warm time savings at higher memory don't offset the extra GB-seconds. Within the free tier, cost differences are negligible regardless.",
        "The adaptor exposes two endpoints so it works as a drop-in replacement for existing integrations:",
        "**OpenAI compatible** (`/v1/audio/transcriptions`): ```bash curl -X POST https:// /v1/audio/transcriptions \\ -H \"Authorization: Token \" \\ -F \"file=@audio.ogg\" \\ -F \"language=yue\" ``` ```json {\"text\": \"transcript here\"} ```",
        "**Deepgram compatible** (`/v1/listen`): ```bash curl -X POST https:// /v1/listen?language=yue \\ -H \"Authorization: Token \" \\ -H \"Content-Type: audio/ogg\" \\ --data-binary @audio.ogg ```",
        "Once you've synced multiple models to EFS, there's no SSH access to see what's there or clean up. I added two non-standard endpoints:",
        "**List models on EFS:** ```bash curl https:// /v1/models -H \"Authorization: Token \" ``` ```json { \"object\": \"list\", \"data\": [ {\"id\": \"openai/whisper-large-v3-turbo\", \"object\": \"model\", \"owned_by\": \"openai\"}, {\"id\": \"Systran/faster-distil-whisper-large-v3\", \"object\": \"model\", \"owned_by\": \"Systran\"} ] } ```",
        "**Delete a model from EFS** (the currently loaded model returns 409): ```bash curl -X DELETE https:// /v1/models/Systran/faster-distil-whisper-large-v3 \\ -H \"Authorization: Token \" ``` ```json {\"id\": \"Systran/faster-distil-whisper-large-v3\", \"object\": \"model\", \"deleted\": true} ```",
        "Slashes in model IDs work naturally — `rawPath` preserves the full path, so `DELETE /v1/models/openai/whisper-large-v3-turbo` correctly maps to model ID `openai/whisper-large-v3-turbo`.",
        "Performance Tip: Always Specify Language",
        "When no language is specified, Whisper runs language detection on the first audio chunk — adding noticeable overhead. For a 2.5s voice message:",
        "| Request | Response Time | |---------|--------------| | No language (auto-detect) | ~22s | | `language=yue` (Cantonese) | ~10s |",
        "That's a **2x speedup** just from passing a language hint. Two ways to do it:",
        "**Option A — per-request query param** (recommended, keeps Lambda language-agnostic): ```bash Deepgram endpoint curl -X POST https:// /v1/listen?language=yue \\ -H \"Authorization: Token \" \\ -H \"Content-Type: audio/ogg\" \\ --data-binary @audio.ogg",
        "OpenAI endpoint curl -X POST https:// /v1/audio/transcriptions \\ -H \"Authorization: Token \" \\ -F \"file=@audio.ogg\" \\ -F \"language=yue\" ```",
        "**Option B — Lambda env var** (simpler if you only ever transcribe one language): ```bash WHISPER_LANGUAGE=yue ```",
        "I use Option A — the language is set in my OpenClaw config (`language: \"yue\"` in the audio model), which passes it as `?language=yue` to the Lambda on every request."
      ]
    },
    {
      "id": "article:i-squeezed-my-1k-monthly-openclaw-api-bill-with-20month-in-aws-credits-heres-the-exact-setup-3gj4",
      "source_type": "article",
      "title": "I Squeezed My $1k Monthly OpenClaw API Bill with ~$20/Month in AWS Credits — Here's the Exact Setup",
      "url": "https://gabrielkoo.com/blog/i-squeezed-my-1k-monthly-openclaw-api-bill-with-20month-in-aws-credits-heres-the-exact-setup-3gj4/",
      "canonical_url": "https://dev.to/aws-builders/i-squeezed-my-1k-monthly-openclaw-api-bill-with-20month-in-aws-credits-heres-the-exact-setup-3gj4",
      "published_at": "2026-02-21",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "bedrock",
        "openclaw",
        "kiro"
      ],
      "description": "I've got OpenClaw running locally on a Raspberry Pi — where computation power is scarce, and it's...",
      "content": "I've got OpenClaw(MoltBot(ClawdBot)) running locally on a Raspberry Pi — where computation power is scarce, and it's gone unresponsive on me more than a few times. But even on constrained hardware, every chat turn, every memory search, every web lookup is hitting paid APIs. The bill is small at first, then it isn't. I had been using `qwen3-coder-480b` for a week or two, and the daily cost skyrocketed to as much as $50.\n\n> **Assumption:** OpenClaw is running on hardware you already own or pay for separately — a Raspberry Pi, home server, or existing cloud instance. The compute cost of the host itself isn't counted in the ~$20/month figure here.\n\nIf you've picked up AWS Credits from events, the [AWS Community Builder program](https://builder.aws.com/content/32g2lQ7kc3Py8kKIYGS15Pe8VSS/aws-community-builders-program) ($500/year), or AWS Activate — or if your company prefers to keep spend within AWS rather than onboarding yet another SaaS API provider — there's a way to run the whole OpenClaw stack on credits.\n\nThis is how I did it.\n\n> **Disclaimer**: The crux of this hack relies heavily on Amazon Q Developer Pro's undocumented while generously high usage ceiling while it lasts. If it's eventually deprecated, we will still need to switch to Kiro plans with overage pricings - still covered by AWS Credits with lower cost/token ratio. \n\n---\n\n## Who This Is For\n\nTwo very different reasons to care about this setup.\n\n**If you have AWS Credits to burn:**\nCredits from re:Invent, AWS Community Builder, AWS Activate, or customer programs come with expiry dates. Running your AI assistant stack on them is one of the most practical ways to put idle credits to work — at ~$20/month, $100 in credits covers 5 months of the full stack. If you're sitting on a few hundred dollars with an end-of-year deadline, this is a productive use before they lapse.\n\n**If you're in a company with procurement or compliance requirements:**\nEvery new SaaS vendor is a TPRM exercise. OpenAI for embeddings, Perplexity for web search, Anthropic for Claude — each one is a separate vendor assessment, a separate DPA, and a separate conversation with your security team. For FSI and regulated industries, that's not just overhead — it can be a blocker.\n\nAWS is likely already in your vendor register. Consolidating on Bedrock means single billing, fewer third-party relationships to manage, and data residency you control. For anything touching customer data in banking, insurance, or healthcare, that's the difference between a quick internal approval and a 3-month procurement cycle.\n\n---\n\n## Prerequisites\n\n- **AWS account** with Bedrock access enabled in `us-east-1` (or another US region)\n- **AWS credentials** — a [Bedrock API key](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) is the simplest option if your account supports it. Otherwise, a long-term IAM access key/secret key pair works fine and is easier to manage than SSO. IAM Identity Center is only required for the Q Developer Pro layer.\n- **Python 3.10+** — used by kiro-gateway, LiteLLM, and the Nova grounding proxy\n- **Amazon Q Developer Pro subscription** ($19/user/month, credit-eligible) — required for Layer 1 (kiro-gateway). Kiro Pro, Pro+, or Power plans also work but are credit-based with overage charges — Q Developer Pro is the better deal.\n\n---\n\n## What Actually Costs Money in OpenClaw?\n\nBefore reaching for solutions, it helps to know exactly where the spend goes. OpenClaw has four distinct cost centers:\n\n**1. Main model (LLM)**\nEvery chat turn, every agent action, every tool call — all routed through your primary LLM. This is the biggest variable cost. On a busy day it adds up fast.\n\n**2. Memory search (embeddings)**\nOpenClaw's `memory_search` tool converts your memory files into vector embeddings and queries them semantically. Every search = an embedding API call. Low cost per call, but it runs constantly in the background.\n\n**3. Web search**\nThe `web_search` tool hits Perplexity or Brave APIs. Perplexity charges per query on paid plans; Brave gives you $5/month free then charges beyond that.\n\n**4. Browser automation**\nThe `browser` tool spins up a Chromium instance for web scraping, form filling, and screenshots. Running a full browser on a low-compute machine (Raspberry Pi, t4g.small) is heavy — and cloud browser options cost per session.\n\nThat's it. Four layers. The goal: drive variable cost to zero.\n\n---\n\n## My Config: All 4 Layers on AWS Credits\n\nHere's the full picture before we go deep:\n\n| Layer | Solution | Credit |\n|-------|----------|--------|\n| Main model | [kiro-gateway](https://github.com/jwadow/kiro-gateway) → Amazon Q Developer Pro | [@Jwadow](https://github.com/Jwadow) |\n| Memory search | Native Bedrock embeddings via [PR #20191](https://github.com/openclaw/openclaw/pull/20191) | [@gabrielkoo](https://github.com/gabrielkoo) |\n| Web search | [bedrock-web-search-proxy](https://github.com/gabrielkoo/bedrock-web-search-proxy) — Nova Grounding as Perplexity drop-in | [@gabrielkoo](https://github.com/gabrielkoo) |\n| Browser | [agent-browser + AgentCore provider](https://github.com/vercel-labs/agent-browser/pull/397) | [@pahudnet](https://x.com/pahudnet) |\n\nTwo of these I built myself. Two were built by other community members. All four are open source.\n\n---\n\n## Layer 1: Main Model + Image Analysis — Kiro CLI — Covered by AWS Credits\n\n### Amazon Q Developer Pro: flat-rate access to Claude\n\nThe key difference between Amazon Q Developer Pro and Kiro Pro is the billing model. Kiro Pro is credit-based — 1,000 credits/month, pay more if you exceed them. Amazon Q Developer Pro is a flat monthly subscription: **$19/user/month, no per-token billing, no surprise overages.**\n\n| Plan | Cost | Usage |\n|------|------|-------|\n| Kiro Free | $0/mo | 50 credits/month |\n| Kiro Pro | $20/mo | 1,000 credits + $0.04/credit overage |\n| Kiro Pro+ | $40/mo | 2,000 credits + $0.04/credit overage |\n| Kiro Power | $200/mo | 10,000 credits + $0.04/credit overage |\n| Amazon Q Developer Pro (legacy) | $19/user/mo | Flat-rate, not credit-capped |\n\n> **Note:** Amazon Q Developer Pro is now a legacy plan in the Kiro ecosystem. AWS has stopped allowing new Builder ID subscriptions to Q Developer Pro — new users can only subscribe through Kiro plans. The undocumented usage limits on Q Pro are likely part of why AWS made this transition. If you're already on Q Developer Pro, you retain access and it remains the better deal for OpenClaw.\n\nYour Q Developer Pro subscription grants access to `kiro-cli`. The documented quota is [10,000 inference calls/month](https://docs.aws.amazon.com/general/latest/gr/amazonqdev.html) — for a personal AI assistant, that's more than enough.\n\n> **Real-world cost check:** In 4 days of active OpenClaw usage after switching to kiro-gateway, I consumed ~40M input tokens and ~865K output tokens with Claude Sonnet. OpenClaw loads memory files, system prompts, and tool results into every turn — the context window fills up fast. At standard Bedrock pricing ($3/1M input, $15/1M output), that's ~$135 for 4 days, or roughly **$1,000/month**. Q Developer Pro covers all of it for $19/month flat.\n\nIn practice, I've been running Kiro CLI with OpenClaw daily and haven't hit any rate limits in active use. Note: the `/usage` command isn't available under the Q Developer Pro plan — monitor your usage via the AWS console instead. That said, after running OpenClaw with kiro-gateway for several days, I checked the Q Developer usage metrics in the AWS console and the figures hadn't moved at all. It's unclear whether Kiro CLI usage is counted against the same quota as Q Developer's agentic requests, or tracked separately. The [Amazon Q Developer pricing page](https://aws.amazon.com/q/developer/pricing/) only states \"Included (with limits)\" for the Pro tier — no specifics on what those limits are or how Kiro CLI calls are metered.\n\n> **Note:** Q Developer Pro requires [AWS IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html) (SSO) — you can't use it with a free Builder ID. If you're already set up with Identity Center (common in enterprise teams and AWS Community Builders with corporate accounts), you're good to go.\n\n> **Important:** Standard AWS Credits don't cover per-token Claude usage via Anthropic's marketplace agreement. But the Q Developer Pro subscription fee itself **is** credit-eligible — making the whole stack fundable with AWS credits. Kiro's flat-rate subscription is currently the only practical way to run Claude in OpenClaw without per-token billing.\n\n> **New AWS accounts:** Even if you'd prefer to pay per-token via direct Bedrock API, new accounts often come with [ultra-low default rate limits](https://dev.to/aws-builders/ultra-low-bedrock-llm-rate-limits-for-new-aws-accounts-time-to-wake-up-your-inactive-aws-accounts-3no0) that can't reliably serve OpenClaw — even when you're willing to pay. The flat-rate Q Developer Pro route sidesteps this entirely.\n\n### kiro-gateway: the bridge\n\n[kiro-gateway](https://github.com/jwadow/kiro-gateway) — built by [@Jwadow](https://github.com/Jwadow) — wraps Kiro CLI and exposes OpenAI-compatible and Anthropic-compatible API endpoints. OpenClaw talks to it like any other provider.\n\n```bash\ngit clone https://github.com/jwadow/kiro-gateway\ncd kiro-gateway\npip install -r requirements.txt\ncp .env.example .env\n```\n\nEdit `.env`:\n\n```env\nPROXY_API_KEY=\"your-secret-key\"\nKIRO_CREDS_FILE=\"~/.aws/sso/cache/kiro-auth-token.json\"\n```\n\nRun `kiro-cli login` once to authenticate — this populates `KIRO_CREDS_FILE` automatically. (`kiro-cli` is only needed for this initial login; `kiro-gateway` reads the token it generates. Re-run if your token expires.) Then:\n\n```bash\npython main.py --port 9000\n```\n\n> **Heads up:** kiro-gateway's hardcoded fallback model list may lag behind new Claude releases. If a model isn't showing up at `/v1/models`, add it manually to `FALLBACK_MODELS` in `kiro/config.py`.\n\nAvailable models via Q Developer Pro:\n\n| Model | Best for |\n|-------|---------|\n| `claude-sonnet-4.6` | General tasks, coding, writing |\n| `claude-haiku-4.5` | Fast, lightweight responses |\n| `claude-opus-4.6` | Complex reasoning, long context |\n\nOpenClaw config:\n\n```json\n{\n  \"models\": {\n    \"providers\": {\n      \"kiro\": {\n        \"baseUrl\": \"http://localhost:9000\",\n        \"apiKey\": \"your-secret-key\",\n        \"api\": \"anthropic-messages\"\n      }\n    }\n  },\n  \"agents\": {\n    \"defaults\": {\n      \"model\": {\n        \"primary\": \"kiro/claude-sonnet-4.6\"\n      },\n      \"imageModel\": {\n        \"primary\": \"kiro/claude-sonnet-4.6\"\n      }\n    }\n  }\n}\n```\n\n> **Bonus:** kiro-gateway works with any tool that supports OpenAI or Anthropic APIs — not just OpenClaw. To use it with Claude Code: `ANTHROPIC_BASE_URL=http://localhost:9000` and `ANTHROPIC_API_KEY=your-secret-key`.\n\n---\n\n## Layer 2: Memory Search — Bedrock Embeddings — Covered by AWS Credits\n\nOpenClaw's `memory_search` needs an embedding model. [Amazon Nova Multimodal Embeddings](https://docs.aws.amazon.com/nova/latest/userguide/nova-embeddings.html) costs ~$0.00014 per 1K tokens — fractions of a cent per query, and covered by AWS Credits.\n\nOpenClaw's native Bedrock provider doesn't wire up embeddings cleanly yet — [PR #24892](https://github.com/openclaw/openclaw/pull/24892) - (I made a novice mistake with [PR #20191](https://github.com/openclaw/openclaw/pull/20191)) is pending merge. Until then, you'll need a local OpenAI-compatible proxy in front of Bedrock. Two options:\n\n### Option A: LiteLLM\n\n```yaml\n# litellm_config.yaml\nmodel_list:\n  - model_name: nova-2-multimodal-embeddings-v1.0\n    litellm_params:\n      model: bedrock/amazon.nova-2-multimodal-embeddings-v1:0\n      aws_region_name: us-east-1\n\nlitellm_settings:\n  drop_params: true\n  master_key: \"local-only\"\n```\n\n```bash\npip install 'litellm[proxy]'\nlitellm --config litellm_config.yaml --port 4000\n```\n\n```json\n\"memorySearch\": {\n  \"enabled\": true,\n  \"provider\": \"openai\",\n  \"remote\": { \"baseUrl\": \"http://localhost:4000\", \"apiKey\": \"local-only\" },\n  \"model\": \"nova-2-multimodal-embeddings-v1.0\"\n}\n```\n\n### Option B: bedrock-access-gateway-function-url (serverless, no fixed cost)\n\nMy own fork of the original `bedrock-access-gateway` — deployed as a Lambda Function URL instead of ALB+Fargate, so there's no $16+/month fixed cost. Full writeup: [Use Amazon Bedrock Models with OpenAI SDKs with a Serverless Proxy Endpoint](https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5).\n\n> **Note:** My [PR #222](https://github.com/aws-samples/bedrock-access-gateway/pull/222) for Nova 2 embedding support against the original `bedrock-access-gateway` project has been merged — so my fork pulls from this upstream automatically via `prepare_source.sh`.\n\n```bash\ngit clone --depth=1 https://github.com/gabrielkoo/bedrock-access-gateway-function-url\ncd bedrock-access-gateway-function-url\n./prepare_source.sh\nsam build\nsam deploy --guided\n```\n\nGrab the `FunctionUrl` output after deploy, then:\n\n```json\n\"memorySearch\": {\n  \"enabled\": true,\n  \"provider\": \"openai\",\n  \"remote\": { \"baseUrl\": \"https://<your-function-url>.lambda-url.us-east-1.on.aws\", \"apiKey\": \"your-api-key\" },\n  \"model\": \"amazon.nova-2-multimodal-embeddings-v1:0\"\n}\n```\n\n> **Region note:** `amazon.nova-2-multimodal-embeddings-v1:0` availability varies — check the [Bedrock model availability page](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html). Make sure your IAM credentials have `bedrock:InvokeModel` in your target region.\n\nOnce [PR #24892](https://github.com/openclaw/openclaw/pull/24892) merges, no proxy needed — the config simplifies to:\n\n```json\n\"memorySearch\": {\n  \"enabled\": true,\n  \"provider\": \"bedrock\",\n  \"model\": \"amazon.nova-2-multimodal-embeddings-v1:0\",\n  \"region\": \"us-east-1\"\n}\n```\n\n---\n\n## Layer 3: Web Search — Nova Grounding Proxy — Covered by AWS Credits\n\nI built [`bedrock-web-search-proxy`](https://github.com/gabrielkoo/bedrock-web-search-proxy) — a FastAPI wrapper that makes Bedrock Nova Grounding look like the Perplexity Sonar API. No Perplexity or Brave API key needed. Runs entirely on AWS Credits.\n\nFull writeup: [Drop-in Perplexity Sonar Replacement with AWS Bedrock Nova Grounding](https://dev.to/aws-builders/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9).\n\n### Option A: Run locally\n\n```bash\ngit clone https://github.com/gabrielkoo/bedrock-web-search-proxy\ncd bedrock-web-search-proxy\npip install fastapi uvicorn boto3\nuvicorn main:app --port 7000\n```\n\n### Option B: Lambda Function URL (zero idle cost)\n\nSee the [deployment guide in the repo](https://github.com/gabrielkoo/bedrock-web-search-proxy) — SAM-based, arm64, python3.13. Once deployed, you get a persistent HTTPS endpoint with no local process to manage.\n\nOpenClaw config:\n\n```json\n{\n  \"tools\": {\n    \"web\": {\n      \"search\": {\n        \"provider\": \"perplexity\",\n        \"perplexity\": {\n          \"apiKey\": \"your-proxy-key\",\n          \"baseUrl\": \"http://localhost:7000/v1\",\n          \"model\": \"sonar-pro\"\n        }\n      }\n    }\n  }\n}\n```\n\n> All US Nova CRIS (Cross-Region Inference Services) profiles support web grounding (`us.amazon.nova-premier-v1:0`, `us.amazon.nova-pro-v1:0`, etc.). Native model IDs without the `us.` prefix do NOT work — must use CRIS profiles. Web grounding is US regions only (us-east-1, us-east-2, us-west-2).\n\n---\n\n## Layer 4: Cloud Browser — Bedrock AgentCore — Covered by AWS Credits\n\n[`agent-browser`](https://github.com/vercel-labs/agent-browser) by Vercel Labs, with the AgentCore provider contributed by [Pahud Hsieh](https://github.com/pahud) ([@pahudnet](https://x.com/pahudnet)) — [PR #397](https://github.com/vercel-labs/agent-browser/pull/397).\n\nThe browser runs in AWS — no local Chromium needed. Particularly useful on low-compute instances (Pi, t4g.small) where running a local browser would be too heavy. Covered by AWS Credits.\n\nNode.js and pnpm required. Since [PR #397](https://github.com/vercel-labs/agent-browser/pull/397) isn't merged yet, check out the branch directly:\n\n```bash\ngit clone https://github.com/vercel-labs/agent-browser\ncd agent-browser\ngit fetch origin pull/397/head:agentcore\ngit checkout agentcore\npnpm install && pnpm build\n```\n\nThen use it:\n\n```bash\nagent-browser -p agentcore open https://example.com\nagent-browser close\n```\n\nYour AWS identity needs these IAM permissions:\n\n- `bedrock-agentcore:StartBrowserSession`\n- `bedrock-agentcore:ConnectBrowserAutomationStream`\n- `bedrock-agentcore:StopBrowserSession`\n\n> On a desktop machine with enough RAM, local CDP (OpenClaw's built-in browser) is free and works fine. AgentCore is the play for headless/low-compute setups.\n\n---\n\n## The Cost Math\n\nWithout this setup, Claude Sonnet alone runs ~**$1,000/month** at standard Bedrock pricing — based on real token usage from my own sessions. OpenClaw's large context window (memory files, system prompts, tool results loaded every turn) means the token bill compounds fast.\n\nThe full stack with this setup runs at **~$20/month**:\n\n- **$19/mo** — Amazon Q Developer Pro (flat-rate, covers all LLM calls)\n- **≤$1/mo** — Bedrock embeddings for memory search (Nova 2 at $0.00014/1K tokens)\n\nWeb search and browser automation are covered by AWS Credits — no separate line item.\n\nWith **$100 in AWS Credits**, you cover roughly **5 months** of the full stack. Both the Q Developer Pro subscription and Bedrock embeddings are credit-eligible — if you're an AWS Community Builder, that $500/year allocation more than covers it.\n\n### Where AWS Credits Come From\n\n- **AWS event participant/speaker** — re:Invent, Summit, local user groups\n- **AWS Community Builder** — $500/year for active builders ([builder.aws.com](https://builder.aws.com/content/32g2lQ7kc3Py8kKIYGS15Pe8VSS/aws-community-builders-program)). The application opens a few rounds per year — I'm one of the builders in the program.\n- **AWS Customer Council** — participation typically includes credits\n- **AWS Activate** (startups) — up to $100K\n- **AWS Educate / Academy** — educators and students\n\nCheck your balance: [console.aws.amazon.com/billing/home#/credits](https://console.aws.amazon.com/billing/home#/credits)\n\n---\n\n## Closing\n\nFour layers. Two built by community members, two I built myself. All open source, all running on AWS Credits.\n\nTo be clear: **kiro-gateway is the most crucial piece here.** [@Jwadow](https://github.com/Jwadow) built the bridge that makes Claude accessible without per-token billing — I built the embedding proxy and web search proxy to fill the remaining gaps. [Web search](https://github.com/gabrielkoo/bedrock-web-search-proxy) and [cloud browser](https://github.com/vercel-labs/agent-browser) (Layers 3 and 4) are purely AWS Credits — no subscription, per-token billing well covered by AWS Credits.\n\nIf you're already an AWS Community Builder or have credits sitting in your account, there's no reason to be paying per-token for a personal AI assistant. Wire it up once, and the stack runs itself.\n\nPut those credits to work.",
      "excerpts": [
        "I've got OpenClaw(MoltBot(ClawdBot)) running locally on a Raspberry Pi — where computation power is scarce, and it's gone unresponsive on me more than a few times. But even on constrained hardware, every chat turn, every memory search, every web lookup is hitting paid APIs. The bill is small at first, then it isn't. I had been using `qwen3-coder-480b` for a week or two, and the daily cost skyrocketed to as much as $50.",
        "> **Assumption:** OpenClaw is running on hardware you already own or pay for separately — a Raspberry Pi, home server, or existing cloud instance. The compute cost of the host itself isn't counted in the ~$20/month figure here.",
        "If you've picked up AWS Credits from events, the [AWS Community Builder program](https://builder.aws.com/content/32g2lQ7kc3Py8kKIYGS15Pe8VSS/aws-community-builders-program) ($500/year), or AWS Activate — or if your company prefers to keep spend within AWS rather than onboarding yet another SaaS API provider — there's a way to run the whole OpenClaw stack on credits.",
        "> **Disclaimer**: The crux of this hack relies heavily on Amazon Q Developer Pro's undocumented while generously high usage ceiling while it lasts. If it's eventually deprecated, we will still need to switch to Kiro plans with overage pricings - still covered by AWS Credits with lower cost/token ratio.",
        "Two very different reasons to care about this setup.",
        "**If you have AWS Credits to burn:** Credits from re:Invent, AWS Community Builder, AWS Activate, or customer programs come with expiry dates. Running your AI assistant stack on them is one of the most practical ways to put idle credits to work — at ~$20/month, $100 in credits covers 5 months of the full stack. If you're sitting on a few hundred dollars with an end-of-year deadline, this is a productive use before they lapse.",
        "**If you're in a company with procurement or compliance requirements:** Every new SaaS vendor is a TPRM exercise. OpenAI for embeddings, Perplexity for web search, Anthropic for Claude — each one is a separate vendor assessment, a separate DPA, and a separate conversation with your security team. For FSI and regulated industries, that's not just overhead — it can be a blocker.",
        "AWS is likely already in your vendor register. Consolidating on Bedrock means single billing, fewer third-party relationships to manage, and data residency you control. For anything touching customer data in banking, insurance, or healthcare, that's the difference between a quick internal approval and a 3-month procurement cycle.",
        "- **AWS account** with Bedrock access enabled in `us-east-1` (or another US region) - **AWS credentials** — a [Bedrock API key](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html) is the simplest option if your account supports it. Otherwise, a long-term IAM access key/secret key pair works fine and is easier to manage than SSO. IAM Identity Center is only required for the Q Developer Pro layer. - **Python 3.10+** — used by kiro-gateway, LiteLLM, and the Nova grounding proxy - **Amazon Q Developer Pro subscription** ($19/user/month, credit-eligible) — required for Layer 1 (kiro-gateway). Kiro Pro, Pro+, or Power plans also work but are credit-based with overage charges — Q Developer Pro is the better deal.",
        "What Actually Costs Money in OpenClaw?",
        "Before reaching for solutions, it helps to know exactly where the spend goes. OpenClaw has four distinct cost centers:",
        "**1. Main model (LLM)** Every chat turn, every agent action, every tool call — all routed through your primary LLM. This is the biggest variable cost. On a busy day it adds up fast.",
        "**2. Memory search (embeddings)** OpenClaw's `memory_search` tool converts your memory files into vector embeddings and queries them semantically. Every search = an embedding API call. Low cost per call, but it runs constantly in the background.",
        "**3. Web search** The `web_search` tool hits Perplexity or Brave APIs. Perplexity charges per query on paid plans; Brave gives you $5/month free then charges beyond that.",
        "**4. Browser automation** The `browser` tool spins up a Chromium instance for web scraping, form filling, and screenshots. Running a full browser on a low-compute machine (Raspberry Pi, t4g.small) is heavy — and cloud browser options cost per session.",
        "That's it. Four layers. The goal: drive variable cost to zero.",
        "My Config: All 4 Layers on AWS Credits",
        "Here's the full picture before we go deep:",
        "| Layer | Solution | Credit | |-------|----------|--------| | Main model | [kiro-gateway](https://github.com/jwadow/kiro-gateway) → Amazon Q Developer Pro | [@Jwadow](https://github.com/Jwadow) | | Memory search | Native Bedrock embeddings via [PR #20191](https://github.com/openclaw/openclaw/pull/20191) | [@gabrielkoo](https://github.com/gabrielkoo) | | Web search | [bedrock-web-search-proxy](https://github.com/gabrielkoo/bedrock-web-search-proxy) — Nova Grounding as Perplexity drop-in | [@gabrielkoo](https://github.com/gabrielkoo) | | Browser | [agent-browser + AgentCore provider](https://github.com/vercel-labs/agent-browser/pull/397) | [@pahudnet](https://x.com/pahudnet) |",
        "Two of these I built myself. Two were built by other community members. All four are open source.",
        "Layer 1: Main Model + Image Analysis — Kiro CLI — Covered by AWS Credits",
        "Amazon Q Developer Pro: flat-rate access to Claude",
        "The key difference between Amazon Q Developer Pro and Kiro Pro is the billing model. Kiro Pro is credit-based — 1,000 credits/month, pay more if you exceed them. Amazon Q Developer Pro is a flat monthly subscription: **$19/user/month, no per-token billing, no surprise overages.**",
        "| Plan | Cost | Usage | |------|------|-------| | Kiro Free | $0/mo | 50 credits/month | | Kiro Pro | $20/mo | 1,000 credits + $0.04/credit overage | | Kiro Pro+ | $40/mo | 2,000 credits + $0.04/credit overage | | Kiro Power | $200/mo | 10,000 credits + $0.04/credit overage | | Amazon Q Developer Pro (legacy) | $19/user/mo | Flat-rate, not credit-capped |",
        "> **Note:** Amazon Q Developer Pro is now a legacy plan in the Kiro ecosystem. AWS has stopped allowing new Builder ID subscriptions to Q Developer Pro — new users can only subscribe through Kiro plans. The undocumented usage limits on Q Pro are likely part of why AWS made this transition. If you're already on Q Developer Pro, you retain access and it remains the better deal for OpenClaw.",
        "Your Q Developer Pro subscription grants access to `kiro-cli`. The documented quota is [10,000 inference calls/month](https://docs.aws.amazon.com/general/latest/gr/amazonqdev.html) — for a personal AI assistant, that's more than enough.",
        "> **Real-world cost check:** In 4 days of active OpenClaw usage after switching to kiro-gateway, I consumed ~40M input tokens and ~865K output tokens with Claude Sonnet. OpenClaw loads memory files, system prompts, and tool results into every turn — the context window fills up fast. At standard Bedrock pricing ($3/1M input, $15/1M output), that's ~$135 for 4 days, or roughly **$1,000/month**. Q Developer Pro covers all of it for $19/month flat.",
        "In practice, I've been running Kiro CLI with OpenClaw daily and haven't hit any rate limits in active use. Note: the `/usage` command isn't available under the Q Developer Pro plan — monitor your usage via the AWS console instead. That said, after running OpenClaw with kiro-gateway for several days, I checked the Q Developer usage metrics in the AWS console and the figures hadn't moved at all. It's unclear whether Kiro CLI usage is counted against the same quota as Q Developer's agentic requests, or tracked separately. The [Amazon Q Developer pricing page](https://aws.amazon.com/q/developer/pricing/) only states \"Included (with limits)\" for the Pro tier — no specifics on what those limits are or how Kiro CLI calls are metered.",
        "> **Note:** Q Developer Pro requires [AWS IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html) (SSO) — you can't use it with a free Builder ID. If you're already set up with Identity Center (common in enterprise teams and AWS Community Builders with corporate accounts), you're good to go.",
        "> **Important:** Standard AWS Credits don't cover per-token Claude usage via Anthropic's marketplace agreement. But the Q Developer Pro subscription fee itself **is** credit-eligible — making the whole stack fundable with AWS credits. Kiro's flat-rate subscription is currently the only practical way to run Claude in OpenClaw without per-token billing.",
        "> **New AWS accounts:** Even if you'd prefer to pay per-token via direct Bedrock API, new accounts often come with [ultra-low default rate limits](https://dev.to/aws-builders/ultra-low-bedrock-llm-rate-limits-for-new-aws-accounts-time-to-wake-up-your-inactive-aws-accounts-3no0) that can't reliably serve OpenClaw — even when you're willing to pay. The flat-rate Q Developer Pro route sidesteps this entirely.",
        "[kiro-gateway](https://github.com/jwadow/kiro-gateway) — built by [@Jwadow](https://github.com/Jwadow) — wraps Kiro CLI and exposes OpenAI-compatible and Anthropic-compatible API endpoints. OpenClaw talks to it like any other provider.",
        "Run `kiro-cli login` once to authenticate — this populates `KIRO_CREDS_FILE` automatically. (`kiro-cli` is only needed for this initial login; `kiro-gateway` reads the token it generates. Re-run if your token expires.) Then:",
        "> **Heads up:** kiro-gateway's hardcoded fallback model list may lag behind new Claude releases. If a model isn't showing up at `/v1/models`, add it manually to `FALLBACK_MODELS` in `kiro/config.py`.",
        "Available models via Q Developer Pro:",
        "| Model | Best for | |-------|---------| | `claude-sonnet-4.6` | General tasks, coding, writing | | `claude-haiku-4.5` | Fast, lightweight responses | | `claude-opus-4.6` | Complex reasoning, long context |",
        "> **Bonus:** kiro-gateway works with any tool that supports OpenAI or Anthropic APIs — not just OpenClaw. To use it with Claude Code: `ANTHROPIC_BASE_URL=http://localhost:9000` and `ANTHROPIC_API_KEY=your-secret-key`.",
        "Layer 2: Memory Search — Bedrock Embeddings — Covered by AWS Credits",
        "OpenClaw's `memory_search` needs an embedding model. [Amazon Nova Multimodal Embeddings](https://docs.aws.amazon.com/nova/latest/userguide/nova-embeddings.html) costs ~$0.00014 per 1K tokens — fractions of a cent per query, and covered by AWS Credits.",
        "OpenClaw's native Bedrock provider doesn't wire up embeddings cleanly yet — [PR #24892](https://github.com/openclaw/openclaw/pull/24892) - (I made a novice mistake with [PR #20191](https://github.com/openclaw/openclaw/pull/20191)) is pending merge. Until then, you'll need a local OpenAI-compatible proxy in front of Bedrock. Two options:",
        "litellm_settings: drop_params: true master_key: \"local-only\" ```",
        "Option B: bedrock-access-gateway-function-url (serverless, no fixed cost)",
        "My own fork of the original `bedrock-access-gateway` — deployed as a Lambda Function URL instead of ALB+Fargate, so there's no $16+/month fixed cost. Full writeup: [Use Amazon Bedrock Models with OpenAI SDKs with a Serverless Proxy Endpoint](https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5).",
        "> **Note:** My [PR #222](https://github.com/aws-samples/bedrock-access-gateway/pull/222) for Nova 2 embedding support against the original `bedrock-access-gateway` project has been merged — so my fork pulls from this upstream automatically via `prepare_source.sh`.",
        "Grab the `FunctionUrl` output after deploy, then:",
        "> **Region note:** `amazon.nova-2-multimodal-embeddings-v1:0` availability varies — check the [Bedrock model availability page](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html). Make sure your IAM credentials have `bedrock:InvokeModel` in your target region.",
        "Once [PR #24892](https://github.com/openclaw/openclaw/pull/24892) merges, no proxy needed — the config simplifies to:",
        "Layer 3: Web Search — Nova Grounding Proxy — Covered by AWS Credits",
        "I built [`bedrock-web-search-proxy`](https://github.com/gabrielkoo/bedrock-web-search-proxy) — a FastAPI wrapper that makes Bedrock Nova Grounding look like the Perplexity Sonar API. No Perplexity or Brave API key needed. Runs entirely on AWS Credits.",
        "Full writeup: [Drop-in Perplexity Sonar Replacement with AWS Bedrock Nova Grounding](https://dev.to/aws-builders/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9).",
        "Option B: Lambda Function URL (zero idle cost)",
        "See the [deployment guide in the repo](https://github.com/gabrielkoo/bedrock-web-search-proxy) — SAM-based, arm64, python3.13. Once deployed, you get a persistent HTTPS endpoint with no local process to manage.",
        "> All US Nova CRIS (Cross-Region Inference Services) profiles support web grounding (`us.amazon.nova-premier-v1:0`, `us.amazon.nova-pro-v1:0`, etc.). Native model IDs without the `us.` prefix do NOT work — must use CRIS profiles. Web grounding is US regions only (us-east-1, us-east-2, us-west-2).",
        "Layer 4: Cloud Browser — Bedrock AgentCore — Covered by AWS Credits",
        "[`agent-browser`](https://github.com/vercel-labs/agent-browser) by Vercel Labs, with the AgentCore provider contributed by [Pahud Hsieh](https://github.com/pahud) ([@pahudnet](https://x.com/pahudnet)) — [PR #397](https://github.com/vercel-labs/agent-browser/pull/397).",
        "The browser runs in AWS — no local Chromium needed. Particularly useful on low-compute instances (Pi, t4g.small) where running a local browser would be too heavy. Covered by AWS Credits.",
        "Node.js and pnpm required. Since [PR #397](https://github.com/vercel-labs/agent-browser/pull/397) isn't merged yet, check out the branch directly:",
        "Your AWS identity needs these IAM permissions:",
        "- `bedrock-agentcore:StartBrowserSession` - `bedrock-agentcore:ConnectBrowserAutomationStream` - `bedrock-agentcore:StopBrowserSession`",
        "> On a desktop machine with enough RAM, local CDP (OpenClaw's built-in browser) is free and works fine. AgentCore is the play for headless/low-compute setups.",
        "Without this setup, Claude Sonnet alone runs ~**$1,000/month** at standard Bedrock pricing — based on real token usage from my own sessions. OpenClaw's large context window (memory files, system prompts, tool results loaded every turn) means the token bill compounds fast.",
        "The full stack with this setup runs at **~$20/month**:",
        "- **$19/mo** — Amazon Q Developer Pro (flat-rate, covers all LLM calls) - **≤$1/mo** — Bedrock embeddings for memory search (Nova 2 at $0.00014/1K tokens)",
        "Web search and browser automation are covered by AWS Credits — no separate line item.",
        "With **$100 in AWS Credits**, you cover roughly **5 months** of the full stack. Both the Q Developer Pro subscription and Bedrock embeddings are credit-eligible — if you're an AWS Community Builder, that $500/year allocation more than covers it.",
        "- **AWS event participant/speaker** — re:Invent, Summit, local user groups - **AWS Community Builder** — $500/year for active builders ([builder.aws.com](https://builder.aws.com/content/32g2lQ7kc3Py8kKIYGS15Pe8VSS/aws-community-builders-program)). The application opens a few rounds per year — I'm one of the builders in the program. - **AWS Customer Council** — participation typically includes credits - **AWS Activate** (startups) — up to $100K - **AWS Educate / Academy** — educators and students",
        "Check your balance: [console.aws.amazon.com/billing/home#/credits](https://console.aws.amazon.com/billing/home#/credits)",
        "Four layers. Two built by community members, two I built myself. All open source, all running on AWS Credits.",
        "To be clear: **kiro-gateway is the most crucial piece here.** [@Jwadow](https://github.com/Jwadow) built the bridge that makes Claude accessible without per-token billing — I built the embedding proxy and web search proxy to fill the remaining gaps. [Web search](https://github.com/gabrielkoo/bedrock-web-search-proxy) and [cloud browser](https://github.com/vercel-labs/agent-browser) (Layers 3 and 4) are purely AWS Credits — no subscription, per-token billing well covered by AWS Credits.",
        "If you're already an AWS Community Builder or have credits sitting in your account, there's no reason to be paying per-token for a personal AI assistant. Wire it up once, and the stack runs itself."
      ]
    },
    {
      "id": "article:drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9",
      "source_type": "article",
      "title": "Drop-in Perplexity Sonar Replacement with AWS Bedrock Nova Grounding",
      "url": "https://gabrielkoo.com/blog/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9/",
      "canonical_url": "https://dev.to/aws-builders/drop-in-perplexity-sonar-replacement-with-aws-bedrock-nova-grounding-35o9",
      "published_at": "2026-02-20",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "bedrock",
        "python",
        "webdev"
      ],
      "description": "If you're running an AI assistant or agent framework that uses Perplexity's Sonar API for web search,...",
      "content": "If you're running an AI assistant or agent framework that uses Perplexity's Sonar API for web search, you're paying per query — or burning through your monthly credit allocation faster than you'd like.\n\nI'm on Perplexity Pro, which comes with $5/month in API credits. Sounds fine until you hit mid-month and realize OpenClaw has quietly burned through all of it. I wanted something uncapped that didn't add another bill. If you're an AWS user with any credits sitting around — that $25 from a workshop, an event promo, or re:Invent swag — there's a better option: route those queries through Amazon Bedrock's Nova Premier grounding instead.\n\nI built [`bedrock-web-search-proxy`](https://github.com/gabrielkoo/bedrock-web-search-proxy), a FastAPI proxy that makes Bedrock Nova Premier look exactly like the Perplexity Sonar API. Change one URL, keep everything else the same.\n\n## What is Nova Grounding?\n\nAmazon Nova Premier supports a `nova_grounding` system tool that lets the model search the web in real-time and return answers with citations — similar to Perplexity Sonar. The difference: it runs on Bedrock, so it counts against your AWS credits rather than a separate Perplexity subscription.\n\n## Why Not Just Use Brave Search's Free Tier?\n\nBrave does have an AI Answers API that returns synthesized answers with citations — similar to Perplexity. Two catches though:\n\n1. **Credit card required** — even the $5/month free tier needs a card on file as an anti-fraud measure\n2. **Undocumented model** — Brave doesn't clearly disclose which LLM powers the answers, so you're trusting a black box\n\nWith Nova grounding, you know exactly what's running (Nova Premier on Bedrock), and it counts against AWS credits you likely already have. No new billing relationship, no mystery model.\n\n## Apps That Use Perplexity API\n\nThe wrapper is a drop-in for any app that supports Perplexity as a provider:\n\n- **OpenClaw** — `tools.web.search.perplexity.baseUrl` config\n- **Open WebUI** — web search integration\n- **LibreChat** — via Perplexity MCP server\n- **Cursor** — Perplexity MCP for web research\n- **Continue.dev** — Sonar models for codebase context\n- **AnythingLLM** — Perplexity as cloud LLM provider\n- **LiteLLM** — web search interception\n\n## Proof It's Actually Grounded (Not Hallucinated)\n\nHere's a direct API call asking for the current Bitcoin price:\n\n```bash\ncurl -s http://localhost:7000/v1/chat/completions \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"nova-premier-web-grounding\",\n    \"messages\": [{\"role\": \"user\", \"content\": \"What is the Bitcoin price right now?\"}],\n    \"max_tokens\": 200\n  }'\n```\n\nResponse:\n\n```json\n{\n  \"choices\": [{\n    \"message\": {\n      \"content\": \"The current price of Bitcoin (BTC) is $67,254.57 USD, reflecting a 0.54% increase in the last 24 hours. Last updated February 20, 2026 at 04:34 UTC.\"\n    }\n  }],\n  \"citations\": [\n    \"https://www.latestly.com/technology/bitcoin-price-today-february-20-2026-btc-price-at-usd-67243-up-compared-to-yesterdays-usd-66941-mark-7321498.html\",\n    \"https://www.binance.com/en/price/bitcoin\"\n  ]\n}\n```\n\nThe citation URL contains today's date in the slug. Not hallucinated — Nova Premier actually fetched and synthesized live web content.\n\n## Setup\n\n### 1. Install and run (one line, no cloning needed)\n\n```bash\nuvx --from git+https://github.com/gabrielkoo/bedrock-web-search-proxy bedrock-web-search-proxy\n```\n\nOr run directly from the raw script:\n\n```bash\nuv run https://raw.githubusercontent.com/gabrielkoo/bedrock-web-search-proxy/main/main.py\n```\n\nBoth require [uv](https://docs.astral.sh/uv/) and AWS credentials with `bedrock:InvokeModel` on `us.amazon.nova-premier-v1:0`. Region defaults to `us-east-1` — override with `AWS_DEFAULT_REGION` if needed.\n\n### 2. Configure your app\n\nFor **OpenClaw**, update `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"tools\": {\n    \"web\": {\n      \"search\": {\n        \"provider\": \"perplexity\",\n        \"perplexity\": {\n          \"baseUrl\": \"http://localhost:7000/v1\",\n          \"apiKey\": \"nova-grounding\",\n          \"model\": \"nova-premier-web-grounding\"\n        }\n      }\n    }\n  }\n}\n```\n\n> ⚠️ The `apiKey` must **not** be a real `pplx-` key — OpenClaw detects that prefix and overrides `baseUrl` back to Perplexity's servers.\n\nFor other apps, just point the Perplexity base URL to `http://your-host:7000/v1` and use any model name — the wrapper routes everything to Nova Premier.\n\n## Model Aliases\n\nAll standard Perplexity model names are accepted and routed to Nova Premier (the only Nova model that currently supports the grounding tool):\n\n| Request model | Bedrock model |\n|---|---|\n| `nova-premier-web-grounding` | `us.amazon.nova-premier-v1:0` |\n| `sonar-pro`, `sonar-pro-online` | `us.amazon.nova-premier-v1:0` |\n| `sonar`, `sonar-mini`, `sonar-turbo` | `us.amazon.nova-premier-v1:0` |\n\n## Cost\n\nNova Premier usage counts against your AWS credits — so if you have any sitting around (a $25 promo from an AWS event, workshop, or re:Invent swag bag), this is effectively free. Check your [Billing console](https://console.aws.amazon.com/billing/home#/credits) — you might have more than you think.\n\n- **AWS Community Builders**: covered by $500/year credits\n- **Others with AWS credits**: same deal — credits apply\n- **No credits**: check the [Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) for current Nova Premier rates\n\n## Caveats\n\n- **Streaming doesn't return `citations[]`** — Nova limitation. Non-streaming works fine, and OpenClaw's `web_search` tool uses non-streaming.\n- **`MAX_CONCURRENT` semaphore** defaults to 5 — tune via env var if needed.\n- **Region**: Nova Premier grounding requires `us-east-1`.\n\n## Wrapping Up\n\nIf you're already on AWS Bedrock for your LLM workloads, there's no reason to pay Perplexity separately for web-grounded search. The wrapper is ~350 lines of Python, has 44 tests, and is OpenAI SDK-compatible — so it works with anything that speaks the Perplexity or OpenAI chat completions API.\n\nRepo: [github.com/gabrielkoo/bedrock-web-search-proxy](https://github.com/gabrielkoo/bedrock-web-search-proxy)",
      "excerpts": [
        "If you're running an AI assistant or agent framework that uses Perplexity's Sonar API for web search, you're paying per query — or burning through your monthly credit allocation faster than you'd like.",
        "I'm on Perplexity Pro, which comes with $5/month in API credits. Sounds fine until you hit mid-month and realize OpenClaw has quietly burned through all of it. I wanted something uncapped that didn't add another bill. If you're an AWS user with any credits sitting around — that $25 from a workshop, an event promo, or re:Invent swag — there's a better option: route those queries through Amazon Bedrock's Nova Premier grounding instead.",
        "I built [`bedrock-web-search-proxy`](https://github.com/gabrielkoo/bedrock-web-search-proxy), a FastAPI proxy that makes Bedrock Nova Premier look exactly like the Perplexity Sonar API. Change one URL, keep everything else the same.",
        "Amazon Nova Premier supports a `nova_grounding` system tool that lets the model search the web in real-time and return answers with citations — similar to Perplexity Sonar. The difference: it runs on Bedrock, so it counts against your AWS credits rather than a separate Perplexity subscription.",
        "Why Not Just Use Brave Search's Free Tier?",
        "Brave does have an AI Answers API that returns synthesized answers with citations — similar to Perplexity. Two catches though:",
        "1. **Credit card required** — even the $5/month free tier needs a card on file as an anti-fraud measure 2. **Undocumented model** — Brave doesn't clearly disclose which LLM powers the answers, so you're trusting a black box",
        "With Nova grounding, you know exactly what's running (Nova Premier on Bedrock), and it counts against AWS credits you likely already have. No new billing relationship, no mystery model.",
        "The wrapper is a drop-in for any app that supports Perplexity as a provider:",
        "- **OpenClaw** — `tools.web.search.perplexity.baseUrl` config - **Open WebUI** — web search integration - **LibreChat** — via Perplexity MCP server - **Cursor** — Perplexity MCP for web research - **Continue.dev** — Sonar models for codebase context - **AnythingLLM** — Perplexity as cloud LLM provider - **LiteLLM** — web search interception",
        "Proof It's Actually Grounded (Not Hallucinated)",
        "Here's a direct API call asking for the current Bitcoin price:",
        "The citation URL contains today's date in the slug. Not hallucinated — Nova Premier actually fetched and synthesized live web content.",
        "1. Install and run (one line, no cloning needed)",
        "Or run directly from the raw script:",
        "Both require [uv](https://docs.astral.sh/uv/) and AWS credentials with `bedrock:InvokeModel` on `us.amazon.nova-premier-v1:0`. Region defaults to `us-east-1` — override with `AWS_DEFAULT_REGION` if needed.",
        "For **OpenClaw**, update `~/.openclaw/openclaw.json`:",
        "> ⚠️ The `apiKey` must **not** be a real `pplx-` key — OpenClaw detects that prefix and overrides `baseUrl` back to Perplexity's servers.",
        "For other apps, just point the Perplexity base URL to `http://your-host:7000/v1` and use any model name — the wrapper routes everything to Nova Premier.",
        "All standard Perplexity model names are accepted and routed to Nova Premier (the only Nova model that currently supports the grounding tool):",
        "| Request model | Bedrock model | |---|---| | `nova-premier-web-grounding` | `us.amazon.nova-premier-v1:0` | | `sonar-pro`, `sonar-pro-online` | `us.amazon.nova-premier-v1:0` | | `sonar`, `sonar-mini`, `sonar-turbo` | `us.amazon.nova-premier-v1:0` |",
        "Nova Premier usage counts against your AWS credits — so if you have any sitting around (a $25 promo from an AWS event, workshop, or re:Invent swag bag), this is effectively free. Check your [Billing console](https://console.aws.amazon.com/billing/home#/credits) — you might have more than you think.",
        "- **AWS Community Builders**: covered by $500/year credits - **Others with AWS credits**: same deal — credits apply - **No credits**: check the [Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) for current Nova Premier rates",
        "- **Streaming doesn't return `citations[]`** — Nova limitation. Non-streaming works fine, and OpenClaw's `web_search` tool uses non-streaming. - **`MAX_CONCURRENT` semaphore** defaults to 5 — tune via env var if needed. - **Region**: Nova Premier grounding requires `us-east-1`.",
        "If you're already on AWS Bedrock for your LLM workloads, there's no reason to pay Perplexity separately for web-grounded search. The wrapper is ~350 lines of Python, has 44 tests, and is OpenAI SDK-compatible — so it works with anything that speaks the Perplexity or OpenAI chat completions API.",
        "Repo: [github.com/gabrielkoo/bedrock-web-search-proxy](https://github.com/gabrielkoo/bedrock-web-search-proxy)"
      ]
    },
    {
      "id": "article:aws-silently-releases-kimi-k25-and-glm-47-models-to-bedrock-1514",
      "source_type": "article",
      "title": "AWS Silently Releases Kimi K2.5 and GLM 4.7 Models to Bedrock",
      "url": "https://gabrielkoo.com/blog/aws-silently-releases-kimi-k25-and-glm-47-models-to-bedrock-1514/",
      "canonical_url": "https://dev.to/aws-builders/aws-silently-releases-kimi-k25-and-glm-47-models-to-bedrock-1514",
      "published_at": "2026-02-08",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "vibecoding",
        "ai"
      ],
      "description": "[UPDATE 10 Feb 2026] - It seems it was part of AWS’s rollout plan for rolling out open weight models...",
      "content": "**[UPDATE 10 Feb 2026]** - It seems it was part of AWS’s rollout plan for rolling out open weight models for Kiro and Kiro CLI! [Open weight models are here: more choice, more speed, less cost](https://kiro.dev/blog/open-weight-models/)\n\nBut interestingly among the models covered in this article, only DeepSeek v3.2, Minimax 2.1 and Qwen Coder next were covered. Moonshot K2.5 and GLM-4.7 were missing.\n————\nI was refreshing my [Bedrock model catalog](https://amazonbedrockmodels.github.io) script our of random curiosity when a few unfamiliar model IDs showed up in us-east-1. No AWS blog post. No tweet thread. Just a few new entries in the API response.\n\n**If you've been waiting for a Claude-adjacent model you could swap in seamlessly via AWS credits — this is it.**\n\nBut there’s a drawback for early adopters - Do read till the end to learn about the flaw!\n\n**Kimi K2.5** (by Moonshot AI), **GLM 4.7** (by Zhipu AI), and several other new models like DeepSeek 3.2 and Qwen3 Coder Next are now live on Bedrock, all with full support for the Converse API, tool calling, and — in Kimi K2.5's case — native image understanding.\n\n![Image description](/assets/img/44497e3244dd.png)\n\n![Image description](/assets/img/3b89e0bed041.png)\n\n**Quick note:** These models aren't listed in the [AWS Bedrock models-supported documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html), yet they're fully functional via the Converse API. That's the whole \"silent release\" thing — available in production, just not reflected in the canonical docs yet. Worth bookmarking your region's actual model list from the Bedrock console instead of relying solely on the written guides.\n\n## The models: what just landed\n\n![Image description](/assets/img/e1fb65a68813.png)\n\n**Kimi K2.5** ([Moonshot AI blog](https://www.kimi.com/blog/kimi-k2.5.html)) is the eye-catcher here:\n\n- **Tool calling (function calling):** ✓ Fully supported via Bedrock Converse API\n- **Image understanding:** ✓ Native image inputs (base64 or URL)\n- **Code generation:** In my testing, it held its own against Claude 4.5 Sonnet on typical coding prompts — it handled a multi-file refactor of a FastAPI router cleanly on the first try\n- **Bedrock Model ID:** `moonshotai.kimi-k2.5`\n- **Availability:** us-east-1, us-west-2 (and expanding)\n- **Use case fit:** Drop-in replacement for Claude if you're already on AWS credits\n\n**GLM 4.7** ([Zhipu AI blog](https://z.ai/blog/glm-4.7)) fills a quieter but useful role:\n\n- **Tool calling:** ✓ Supported, though less aggressively tested in my flows\n- **Code generation:** Strong; competitive with Deepseek for certain workloads\n- **Bedrock Model ID:** `zai.glm-4.7(-flash)`\n- **Availability:** us-east-1, us-west-2\n- **Use case fit:** Solid all-arounder; good for prompts that don't strictly require image handling\n\n**The real unlock:** Both are live on `converse` API, which means they work seamlessly with Bedrock's function-calling infrastructure. \n\n### When to pick which\n\n| Need | Pick | Why |\n|------|------|-----|\n| Image understanding + tool calling | **Kimi K2.5** | Only Bedrock open-weight flagship model with both |\n| Text-only tasks, cost-conscious | **GLM 4.7** | Solid all-arounder, no vision overhead |\n| Maximum reliability & ecosystem | **Claude** | Battle-tested, widest documentation |\n\n## Why this matters for vibe coding\n\n*\"Vibe coding\"* — the practice of rapidly iterating on code with LLM assistance, swapping models mid-session, and optimizing for flow over perfection — lives or dies on how frictionless your model-switching is.\n\nIf you're sitting on expiring AWS credits (I've got ~$700 by July 2026), the bottleneck isn't usually \"which model is smartest?\" — it's \"how fast can I swap without rewriting everything?\"\n\nKimi K2.5 solves a real pain point: until now, if you wanted image understanding + tool calling + AWS-native billing, you were stuck with Claude. And the only way you could have done so is via purchasing Kiro CLI subscription ($20/$40/$200 per month) - Since Anthropic Claude models are not covered by the typical AWS Credits.\n\nI initially considered subscribing to the $200 Kiro Power plan, but then I am not confident that I could utilize the plan fully every month, but I'm worried that sticking to the $20/$40 plans will result to paying for Kiro credit overages (which is double of the average plan price per credit). Therefore any PAYG option would perfectly fit my usage pattern.\n\nSo now you have an option that:\n\n1. **Bills directly to your AWS account** — no vendor intermediary, no separate API key, just your existing credits burning down\n2. Runs on the same Bedrock Converse API \n3. Calls tools reliably\n4. Natively understands images\n\nFor experimentation loops (refactors, code generation, visual analysis), that's a genuinely useful escape hatch.\n\n## The lightweight setup: local LiteLLM gateway\n\nYou don't need a complex setup. My entire gateway is:\n\n- A Python venv with `litellm` installed\n- A single YAML config file (shown in the next section)\n- A systemd unit to keep it running on port 4000\n\nNo containers, no Kubernetes. One command to install, one service file to manage. Once running, any client on that machine calls `http://localhost:4000/chat/completions` with the standard OpenAI format, and LiteLLM translates it to Bedrock Converse API automatically.\n\n**Performance note:** In my testing, the LiteLLM translation layer adds negligible latency (~20–50ms overhead). Streaming responses from Kimi K2.5 feel comparable to calling Claude directly — first tokens arrive within 1–2 seconds for typical prompts.\n\n## Bonus: Claude Code & OpenCode integration\n\nHere's the slightly cheeky part: you can point [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [OpenCode](https://opencode.ai/) at your local LiteLLM gateway and route requests through to Kimi K2.5 or GLM 4.7 on Bedrock — all while staying on your AWS credits.\n\nLiteLLM supports the Anthropic `/v1/messages` API endpoint, so it's a two-liner to set up:\n\n```bash\nexport ANTHROPIC_BASE_URL=http://localhost:4000\nexport ANTHROPIC_AUTH_TOKEN=sk-your-litellm-key\nexport ANTHROPIC_MODEL=kimi-k2.5\nexport DISABLE_PROMPT_CACHING=true\n```\n\nThe `DISABLE_PROMPT_CACHING=true` is essential here (Special thanks my colleague _**[at]Marty**_ for troubleshooting and fixing that) - since by default Claude Code tries to apply prompt caching for speed, but not every model on Amazon Bedrock supports that:\n\n![Image description](/assets/img/b70d22146480.png)\n\n\n\nThen launch Claude Code or OpenCode as usual. LiteLLM intercepts the Anthropic-format requests and translates them to Bedrock Converse calls. It's not officially blessed by Anthropic, but it works cleanly for local experimentation — and your AWS credits take the hit instead of your Anthropic billing.\n\n![Image description](/assets/img/00e6c23d9623.png)\n\n\n\n## Config: explicit about capabilities\n\nHere's how I route Kimi K2.5 and GLM 4.7:\n\n```yaml\nmodel_list:\n  - model_name: kimi-k2.5\n    litellm_params:\n      model: bedrock/converse/moonshotai.kimi-k2.5\n      aws_region_name: us-east-1\n      allowed_openai_params: ['reasoning_effort', 'tools', 'tool_choice']\n    model_info:\n      mode: completion\n\n  - model_name: glm-4.7\n    litellm_params:\n      model: bedrock/converse/zai.glm-4.7\n      aws_region_name: us-east-1\n      allowed_openai_params: ['reasoning_effort', 'tools', 'tool_choice']    \n    model_info:\n      mode: completion\n\nlitellm_settings:\n  modify_params: true\n  log_responses: true\n```\n\nKey patterns here:\n\n- **Friendly names** (`kimi-k2.5`, `glm-4.7`) instead of long model IDs\n- **Explicit capability flags** (`supports_function_calling`, `supports_vision`)\n- **`modify_params: true`** for Bedrock edge-case smoothing\n- **Single region** (us-east-1) since both models are there\n\nThe capability flags matter. They let your orchestration layer (or agent framework) gracefully degrade if a model can't do tools or images. No more \"half-attempt to call a function and fail mysteriously.\"\n\n## Testing: quick verification\n\nI tested both models with the Bedrock Converse API in us-east-1. Here's what actually happened:\n\n**Kimi K2.5:** I threw a \"get current weather in Tokyo\" tool spec at it — standard JSON Schema function definition, nothing fancy. It correctly structured the function call on the first attempt, including proper argument types in the response. For code generation, I asked it to refactor a Python CLI script into async; the output was clean and ran without edits. Image support is declared in the model schema but I haven't validated it hands-on yet — that's next on my list.\n\n**GLM 4.7:** Solid on text queries and code generation. Tool calling works, though it was slightly less eager to invoke tools unprompted compared to Kimi — it sometimes answered directly when I expected a function call. No image support, as expected; Zhipu hasn't added vision capabilities to GLM 4.7.\n\n## Why the quiet release?\n\nThese aren't show-stopping announcements. They're bread-and-butter additions to Bedrock's model portfolio. AWS likely brought them in as part of an ongoing expansion to reduce vendor lock-in on \"you have to use Claude for everything.\" That's healthy — more options, better pricing pressure, cleaner credit utilization.\n\nThis is a pattern, not an anomaly. Anthropic Claude, AI21 Jamba, and several Mistral variants all appeared on Bedrock before official blog posts or documentation updates. If you're only checking AWS launch announcements, you're always behind.\n\n> **📌 That's exactly why I built [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io)** — a living catalog of what's *actually* available on Bedrock, in which regions, and what each model can do. Bookmark it. It updates faster than the docs.\n\n## The model-swapping checklist\n\nIf you want to swap models without rewriting your code:\n\n1. **Use a gateway** (LiteLLM, LLMProxy, or similar) to normalize requests\n2. **Pin the Bedrock route explicitly** (`bedrock/converse/modelid`) in your config\n3. **Mark capability per model** (tool calling, vision, etc.) — don't assume\n4. **Test the tool spec** — even \"supported\" models sometimes have quirky implementations\n5. **Keep a catalog** so you don't rediscover the same model twice\n\nKimi K2.5 fits this playbook cleanly. It's a genuine Claude replacement for Bedrock users, not a \"wait and see if it works\" experiment.\n\n## Next steps\n\n- **If you're on AWS credits:** Spin up a local LiteLLM instance and try both models\n- **If you find more quietly-available models:** Open a PR against the catalog or message me\n- **If you're in an AWS org:** Check your Bedrock region — availability is still expanding\n\nThe AWS credits will expire whether you use them or not. Might as well pick models that fit your workflow instead of forcing your workflow around Kiro CLI only.\n\n---\n\nP.S. During my testing, indeed I observed occasional longer LLM inference times - but I guess AWS is working on providing more compute capability on these newer beta models.\n\n---\n\n## References\n\n[1] [Moonshot AI — Kimi K2.5 Announcement](https://www.kimi.com/blog/kimi-k2.5.html)\n[2] [Zhipu AI — GLM 4.7 Announcement](https://z.ai/blog/glm-4.7)\n[3] [AWS Bedrock — Converse API Reference](https://docs.aws.amazon.com/cli/latest/reference/bedrock-runtime/converse.html)\n[4] [LiteLLM — AWS Bedrock Provider Documentation](https://docs.litellm.ai/docs/providers/bedrock)\n[5] [Unofficial - Amazon Bedrock Model Catalog I Created](https://amazonbedrockmodels.github.io)",
      "excerpts": [
        "**[UPDATE 10 Feb 2026]** - It seems it was part of AWS’s rollout plan for rolling out open weight models for Kiro and Kiro CLI! [Open weight models are here: more choice, more speed, less cost](https://kiro.dev/blog/open-weight-models/)",
        "But interestingly among the models covered in this article, only DeepSeek v3.2, Minimax 2.1 and Qwen Coder next were covered. Moonshot K2.5 and GLM-4.7 were missing. ———— I was refreshing my [Bedrock model catalog](https://amazonbedrockmodels.github.io) script our of random curiosity when a few unfamiliar model IDs showed up in us-east-1. No AWS blog post. No tweet thread. Just a few new entries in the API response.",
        "**If you've been waiting for a Claude-adjacent model you could swap in seamlessly via AWS credits — this is it.**",
        "But there’s a drawback for early adopters - Do read till the end to learn about the flaw!",
        "**Kimi K2.5** (by Moonshot AI), **GLM 4.7** (by Zhipu AI), and several other new models like DeepSeek 3.2 and Qwen3 Coder Next are now live on Bedrock, all with full support for the Converse API, tool calling, and — in Kimi K2.5's case — native image understanding.",
        "![Image description](/assets/img/44497e3244dd.png)",
        "![Image description](/assets/img/3b89e0bed041.png)",
        "**Quick note:** These models aren't listed in the [AWS Bedrock models-supported documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html), yet they're fully functional via the Converse API. That's the whole \"silent release\" thing — available in production, just not reflected in the canonical docs yet. Worth bookmarking your region's actual model list from the Bedrock console instead of relying solely on the written guides.",
        "![Image description](/assets/img/e1fb65a68813.png)",
        "**Kimi K2.5** ([Moonshot AI blog](https://www.kimi.com/blog/kimi-k2.5.html)) is the eye-catcher here:",
        "- **Tool calling (function calling):** ✓ Fully supported via Bedrock Converse API - **Image understanding:** ✓ Native image inputs (base64 or URL) - **Code generation:** In my testing, it held its own against Claude 4.5 Sonnet on typical coding prompts — it handled a multi-file refactor of a FastAPI router cleanly on the first try - **Bedrock Model ID:** `moonshotai.kimi-k2.5` - **Availability:** us-east-1, us-west-2 (and expanding) - **Use case fit:** Drop-in replacement for Claude if you're already on AWS credits",
        "**GLM 4.7** ([Zhipu AI blog](https://z.ai/blog/glm-4.7)) fills a quieter but useful role:",
        "- **Tool calling:** ✓ Supported, though less aggressively tested in my flows - **Code generation:** Strong; competitive with Deepseek for certain workloads - **Bedrock Model ID:** `zai.glm-4.7(-flash)` - **Availability:** us-east-1, us-west-2 - **Use case fit:** Solid all-arounder; good for prompts that don't strictly require image handling",
        "**The real unlock:** Both are live on `converse` API, which means they work seamlessly with Bedrock's function-calling infrastructure.",
        "| Need | Pick | Why | |------|------|-----| | Image understanding + tool calling | **Kimi K2.5** | Only Bedrock open-weight flagship model with both | | Text-only tasks, cost-conscious | **GLM 4.7** | Solid all-arounder, no vision overhead | | Maximum reliability & ecosystem | **Claude** | Battle-tested, widest documentation |",
        "Why this matters for vibe coding",
        "*\"Vibe coding\"* — the practice of rapidly iterating on code with LLM assistance, swapping models mid-session, and optimizing for flow over perfection — lives or dies on how frictionless your model-switching is.",
        "If you're sitting on expiring AWS credits (I've got ~$700 by July 2026), the bottleneck isn't usually \"which model is smartest?\" — it's \"how fast can I swap without rewriting everything?\"",
        "Kimi K2.5 solves a real pain point: until now, if you wanted image understanding + tool calling + AWS-native billing, you were stuck with Claude. And the only way you could have done so is via purchasing Kiro CLI subscription ($20/$40/$200 per month) - Since Anthropic Claude models are not covered by the typical AWS Credits.",
        "I initially considered subscribing to the $200 Kiro Power plan, but then I am not confident that I could utilize the plan fully every month, but I'm worried that sticking to the $20/$40 plans will result to paying for Kiro credit overages (which is double of the average plan price per credit). Therefore any PAYG option would perfectly fit my usage pattern.",
        "So now you have an option that:",
        "1. **Bills directly to your AWS account** — no vendor intermediary, no separate API key, just your existing credits burning down 2. Runs on the same Bedrock Converse API 3. Calls tools reliably 4. Natively understands images",
        "For experimentation loops (refactors, code generation, visual analysis), that's a genuinely useful escape hatch.",
        "The lightweight setup: local LiteLLM gateway",
        "You don't need a complex setup. My entire gateway is:",
        "- A Python venv with `litellm` installed - A single YAML config file (shown in the next section) - A systemd unit to keep it running on port 4000",
        "No containers, no Kubernetes. One command to install, one service file to manage. Once running, any client on that machine calls `http://localhost:4000/chat/completions` with the standard OpenAI format, and LiteLLM translates it to Bedrock Converse API automatically.",
        "**Performance note:** In my testing, the LiteLLM translation layer adds negligible latency (~20–50ms overhead). Streaming responses from Kimi K2.5 feel comparable to calling Claude directly — first tokens arrive within 1–2 seconds for typical prompts.",
        "Bonus: Claude Code & OpenCode integration",
        "Here's the slightly cheeky part: you can point [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [OpenCode](https://opencode.ai/) at your local LiteLLM gateway and route requests through to Kimi K2.5 or GLM 4.7 on Bedrock — all while staying on your AWS credits.",
        "LiteLLM supports the Anthropic `/v1/messages` API endpoint, so it's a two-liner to set up:",
        "The `DISABLE_PROMPT_CACHING=true` is essential here (Special thanks my colleague _**[at]Marty**_ for troubleshooting and fixing that) - since by default Claude Code tries to apply prompt caching for speed, but not every model on Amazon Bedrock supports that:",
        "![Image description](/assets/img/b70d22146480.png)",
        "Then launch Claude Code or OpenCode as usual. LiteLLM intercepts the Anthropic-format requests and translates them to Bedrock Converse calls. It's not officially blessed by Anthropic, but it works cleanly for local experimentation — and your AWS credits take the hit instead of your Anthropic billing.",
        "![Image description](/assets/img/00e6c23d9623.png)",
        "Config: explicit about capabilities",
        "Here's how I route Kimi K2.5 and GLM 4.7:",
        "- model_name: glm-4.7 litellm_params: model: bedrock/converse/zai.glm-4.7 aws_region_name: us-east-1 allowed_openai_params: ['reasoning_effort', 'tools', 'tool_choice'] model_info: mode: completion",
        "litellm_settings: modify_params: true log_responses: true ```",
        "- **Friendly names** (`kimi-k2.5`, `glm-4.7`) instead of long model IDs - **Explicit capability flags** (`supports_function_calling`, `supports_vision`) - **`modify_params: true`** for Bedrock edge-case smoothing - **Single region** (us-east-1) since both models are there",
        "The capability flags matter. They let your orchestration layer (or agent framework) gracefully degrade if a model can't do tools or images. No more \"half-attempt to call a function and fail mysteriously.\"",
        "I tested both models with the Bedrock Converse API in us-east-1. Here's what actually happened:",
        "**Kimi K2.5:** I threw a \"get current weather in Tokyo\" tool spec at it — standard JSON Schema function definition, nothing fancy. It correctly structured the function call on the first attempt, including proper argument types in the response. For code generation, I asked it to refactor a Python CLI script into async; the output was clean and ran without edits. Image support is declared in the model schema but I haven't validated it hands-on yet — that's next on my list.",
        "**GLM 4.7:** Solid on text queries and code generation. Tool calling works, though it was slightly less eager to invoke tools unprompted compared to Kimi — it sometimes answered directly when I expected a function call. No image support, as expected; Zhipu hasn't added vision capabilities to GLM 4.7.",
        "These aren't show-stopping announcements. They're bread-and-butter additions to Bedrock's model portfolio. AWS likely brought them in as part of an ongoing expansion to reduce vendor lock-in on \"you have to use Claude for everything.\" That's healthy — more options, better pricing pressure, cleaner credit utilization.",
        "This is a pattern, not an anomaly. Anthropic Claude, AI21 Jamba, and several Mistral variants all appeared on Bedrock before official blog posts or documentation updates. If you're only checking AWS launch announcements, you're always behind.",
        "> **📌 That's exactly why I built [amazonbedrockmodels.github.io](https://amazonbedrockmodels.github.io)** — a living catalog of what's *actually* available on Bedrock, in which regions, and what each model can do. Bookmark it. It updates faster than the docs.",
        "If you want to swap models without rewriting your code:",
        "1. **Use a gateway** (LiteLLM, LLMProxy, or similar) to normalize requests 2. **Pin the Bedrock route explicitly** (`bedrock/converse/modelid`) in your config 3. **Mark capability per model** (tool calling, vision, etc.) — don't assume 4. **Test the tool spec** — even \"supported\" models sometimes have quirky implementations 5. **Keep a catalog** so you don't rediscover the same model twice",
        "Kimi K2.5 fits this playbook cleanly. It's a genuine Claude replacement for Bedrock users, not a \"wait and see if it works\" experiment.",
        "- **If you're on AWS credits:** Spin up a local LiteLLM instance and try both models - **If you find more quietly-available models:** Open a PR against the catalog or message me - **If you're in an AWS org:** Check your Bedrock region — availability is still expanding",
        "The AWS credits will expire whether you use them or not. Might as well pick models that fit your workflow instead of forcing your workflow around Kiro CLI only.",
        "P.S. During my testing, indeed I observed occasional longer LLM inference times - but I guess AWS is working on providing more compute capability on these newer beta models.",
        "[1] [Moonshot AI — Kimi K2.5 Announcement](https://www.kimi.com/blog/kimi-k2.5.html) [2] [Zhipu AI — GLM 4.7 Announcement](https://z.ai/blog/glm-4.7) [3] [AWS Bedrock — Converse API Reference](https://docs.aws.amazon.com/cli/latest/reference/bedrock-runtime/converse.html) [4] [LiteLLM — AWS Bedrock Provider Documentation](https://docs.litellm.ai/docs/providers/bedrock) [5] [Unofficial - Amazon Bedrock Model Catalog I Created](https://amazonbedrockmodels.github.io)"
      ]
    },
    {
      "id": "article:ultra-low-bedrock-llm-rate-limits-for-new-aws-accounts-time-to-wake-up-your-inactive-aws-accounts-3no0",
      "source_type": "article",
      "title": "Ultra Low Bedrock LLM Rate Limits for New AWS Accounts? Time to Wake Up Your Inactive AWS Accounts!",
      "url": "https://gabrielkoo.com/blog/ultra-low-bedrock-llm-rate-limits-for-new-aws-accounts-time-to-wake-up-your-inactive-aws-accounts-3no0/",
      "canonical_url": "https://dev.to/aws-builders/ultra-low-bedrock-llm-rate-limits-for-new-aws-accounts-time-to-wake-up-your-inactive-aws-accounts-3no0",
      "published_at": "2025-11-26",
      "last_verified_at": "2026-08-23",
      "tags": [
        "bedrock",
        "ai",
        "aws"
      ],
      "description": "Are You Struggling With Amazon Bedrock’s Ultra-Low Quotas on New AWS Accounts? 🤯 Are you...",
      "content": "## Are You Struggling With Amazon Bedrock’s Ultra-Low Quotas on New AWS Accounts? 🤯\n\nAre you hitting painfully low rate limits when running LLMs on Amazon Bedrock from a newly created AWS account? You’re definitely not alone — many developers are discovering that new accounts often start with **extremely restrictive quotas**, sometimes as low as **2 requests per minute**.\n\nOfficial guidance usually suggests contacting an account manager to escalate your limits, but for startups, hobby projects, or personal experimentation, that path is far from simple.\n\n## New Account? Big Ambitions, Tiny Quota 🤏🚧\n\nSince 2024 or maybe 2025, AWS quietly adjusted Bedrock’s default model access for newly created accounts - together with other lower account defaults such as a maximum concurreny of 10 for AWS Lambda. Even when using global endpoints, many fresh accounts get **just a few requests per minute** (e.g., 2 rpm for Claude 4.5 Sonnet). This severely slows down prototyping or early-stage AI development.\n\nMeanwhile, **older AWS accounts** — even ones that never touched Bedrock before — often start with dramatically higher limits, approaching **200+ rpm** for the exact same models.\n\n![Sonnet 4.5 Rates](/assets/img/21649e128927.png)\n\nThis creates a real operational advantage for teams with access to aged accounts.\n\n## The Elder Account Advantage 🕰️✨\n\nAWS appears to apply significantly stricter defaults to newer accounts while preserving far more permissive limits for older ones. This aligns with AWS’s long-standing pattern of maintaining stable experiences for long-time customers.\n\nIn practice, a dormant but years-old AWS account can immediately receive much **higher Bedrock limits** purely due to its age.\n\nBelow is a striking example: **3 rpm** for new accounts vs **250 rpm** for older accounts on Claude 4.5 Opus.\n\n![Opus 4.5 Rates](/assets/img/ad1a563932cb.png)\n\n## Why AWS Is Unlikely to Reduce Older Accounts’ Limits 🔒🏢\n\nReducing quotas for older accounts would create major risk and break expectations for long-standing customers — especially enterprises.\n\n* Many organizations have stable, long-lived workloads.\n* Retroactively lowering quotas could break pipelines and violate performance assumptions.\n* AWS historically avoids backward-incompatible changes unless absolutely necessary.\n\nBecause of this, it’s unlikely AWS will apply newer, stricter defaults to older accounts. Teams spinning up new AWS accounts face a much steeper ramp for experimenting with Bedrock.\n\n## Finding and Reusing Older AWS Accounts 🔎📦\n\nIf your organization has older AWS accounts lying around, they may offer instant scaling advantages. With the new **[AWS Organizations Direct Account Transfer](https://aws.amazon.com/about-aws/whats-new/2025/11/aws-organizations-direct-account-transfers/)** feature, accounts can move between Organizations without removing payment methods or performing the old, painful detachment workflow.\n\nWhen moving such accounts, remember to update:\n\n* **Legal entity name**\n* **Root user email**\n* **Addresses and billing contacts**\n* **Tax information**\n\nIf your organization uses [Trusted Access for AWS Account Management](https://docs.aws.amazon.com/accounts/latest/reference/using-orgs-trusted-access.html), these updates are straightforward. Also make sure to audit the account for any leftover resources before dedicating it to GenAI workloads.\n\n\n## Why New Accounts? Why Not Run Everything in One AWS Account? 🧩💼\n\nPutting Bedrock experiments, different production services, and dev workloads into a single account sounds convenient — until it isn’t. Combining everything into one single AWS Account creates unnecessary risk and operational noise.\n\n1. Isolation Protects You 🔥🧱\nAccount boundaries are AWS’s strongest safety net. One bad experiment or IAM mistake shouldn’t touch production. Isolation limits accidental data access, cost spikes, and incident blast radius.\n\n2. Governance Stays Clean 📜✨\nDifferent workloads need different controls. Separate accounts keep audits simpler, give clear ownership, and let you apply SCPs and guardrails without compromise.\n\n3. Costs Stay Transparent 💸📊\nMixing Bedrock prototyping with core services muddies cost reporting. Individual accounts let you track usage, set budgets, and avoid team-to-team disputes.\n\n4. Experiments Move Faster ⚡🔬\nA sandbox (ideally an older account with higher limits) lets you test models, tweak IAM, and push boundaries freely — without risking production stability.\n\n5. It Matches AWS Best Practices 🏗️📚\nAWS recommends multi-account setups for lifecycle separation and blast-radius control. Using older accounts for Bedrock while keeping core workloads isolated follows this playbook perfectly.\n\n\n## How to Break Free From Bedrock’s Slow Lane 🚀💡\n\n* Identify older AWS accounts that haven't been used recently.\n* Transfer them into your AWS Organization using the streamlined Direct Account Transfer workflow.\n* Update all account metadata for compliance.\n* Deploy your Bedrock workloads — and unlock higher default limits instantly.\n\nThis approach helps teams accelerate their AI development journey despite the strict constraints placed on newly created AWS accounts.\n\nHave you seen similar quota differences in your environment? Share your experience — more data points help the community understand the pattern! 🙌",
      "excerpts": [
        "Are You Struggling With Amazon Bedrock’s Ultra-Low Quotas on New AWS Accounts? 🤯",
        "Are you hitting painfully low rate limits when running LLMs on Amazon Bedrock from a newly created AWS account? You’re definitely not alone — many developers are discovering that new accounts often start with **extremely restrictive quotas**, sometimes as low as **2 requests per minute**.",
        "Official guidance usually suggests contacting an account manager to escalate your limits, but for startups, hobby projects, or personal experimentation, that path is far from simple.",
        "New Account? Big Ambitions, Tiny Quota 🤏🚧",
        "Since 2024 or maybe 2025, AWS quietly adjusted Bedrock’s default model access for newly created accounts - together with other lower account defaults such as a maximum concurreny of 10 for AWS Lambda. Even when using global endpoints, many fresh accounts get **just a few requests per minute** (e.g., 2 rpm for Claude 4.5 Sonnet). This severely slows down prototyping or early-stage AI development.",
        "Meanwhile, **older AWS accounts** — even ones that never touched Bedrock before — often start with dramatically higher limits, approaching **200+ rpm** for the exact same models.",
        "![Sonnet 4.5 Rates](/assets/img/21649e128927.png)",
        "This creates a real operational advantage for teams with access to aged accounts.",
        "The Elder Account Advantage 🕰️✨",
        "AWS appears to apply significantly stricter defaults to newer accounts while preserving far more permissive limits for older ones. This aligns with AWS’s long-standing pattern of maintaining stable experiences for long-time customers.",
        "In practice, a dormant but years-old AWS account can immediately receive much **higher Bedrock limits** purely due to its age.",
        "Below is a striking example: **3 rpm** for new accounts vs **250 rpm** for older accounts on Claude 4.5 Opus.",
        "![Opus 4.5 Rates](/assets/img/ad1a563932cb.png)",
        "Why AWS Is Unlikely to Reduce Older Accounts’ Limits 🔒🏢",
        "Reducing quotas for older accounts would create major risk and break expectations for long-standing customers — especially enterprises.",
        "* Many organizations have stable, long-lived workloads. * Retroactively lowering quotas could break pipelines and violate performance assumptions. * AWS historically avoids backward-incompatible changes unless absolutely necessary.",
        "Because of this, it’s unlikely AWS will apply newer, stricter defaults to older accounts. Teams spinning up new AWS accounts face a much steeper ramp for experimenting with Bedrock.",
        "Finding and Reusing Older AWS Accounts 🔎📦",
        "If your organization has older AWS accounts lying around, they may offer instant scaling advantages. With the new **[AWS Organizations Direct Account Transfer](https://aws.amazon.com/about-aws/whats-new/2025/11/aws-organizations-direct-account-transfers/)** feature, accounts can move between Organizations without removing payment methods or performing the old, painful detachment workflow.",
        "When moving such accounts, remember to update:",
        "* **Legal entity name** * **Root user email** * **Addresses and billing contacts** * **Tax information**",
        "If your organization uses [Trusted Access for AWS Account Management](https://docs.aws.amazon.com/accounts/latest/reference/using-orgs-trusted-access.html), these updates are straightforward. Also make sure to audit the account for any leftover resources before dedicating it to GenAI workloads.",
        "Why New Accounts? Why Not Run Everything in One AWS Account? 🧩💼",
        "Putting Bedrock experiments, different production services, and dev workloads into a single account sounds convenient — until it isn’t. Combining everything into one single AWS Account creates unnecessary risk and operational noise.",
        "1. Isolation Protects You 🔥🧱 Account boundaries are AWS’s strongest safety net. One bad experiment or IAM mistake shouldn’t touch production. Isolation limits accidental data access, cost spikes, and incident blast radius.",
        "2. Governance Stays Clean 📜✨ Different workloads need different controls. Separate accounts keep audits simpler, give clear ownership, and let you apply SCPs and guardrails without compromise.",
        "3. Costs Stay Transparent 💸📊 Mixing Bedrock prototyping with core services muddies cost reporting. Individual accounts let you track usage, set budgets, and avoid team-to-team disputes.",
        "4. Experiments Move Faster ⚡🔬 A sandbox (ideally an older account with higher limits) lets you test models, tweak IAM, and push boundaries freely — without risking production stability.",
        "5. It Matches AWS Best Practices 🏗️📚 AWS recommends multi-account setups for lifecycle separation and blast-radius control. Using older accounts for Bedrock while keeping core workloads isolated follows this playbook perfectly.",
        "How to Break Free From Bedrock’s Slow Lane 🚀💡",
        "* Identify older AWS accounts that haven't been used recently. * Transfer them into your AWS Organization using the streamlined Direct Account Transfer workflow. * Update all account metadata for compliance. * Deploy your Bedrock workloads — and unlock higher default limits instantly.",
        "This approach helps teams accelerate their AI development journey despite the strict constraints placed on newly created AWS accounts.",
        "Have you seen similar quota differences in your environment? Share your experience — more data points help the community understand the pattern! 🙌"
      ]
    },
    {
      "id": "article:use-openai-codex-cli-with-amazon-bedrock-models-pay-as-you-go-48eb",
      "source_type": "article",
      "title": "Use OpenAI Codex CLI with Amazon Bedrock Models - Pay As You Go",
      "url": "https://gabrielkoo.com/blog/use-openai-codex-cli-with-amazon-bedrock-models-pay-as-you-go-48eb/",
      "canonical_url": "https://dev.to/aws-builders/use-openai-codex-cli-with-amazon-bedrock-models-pay-as-you-go-48eb",
      "published_at": "2025-08-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "vibecoding",
        "genai",
        "aws"
      ],
      "description": "OpenAI Codex CLI on Amazon Bedrock Models: Why Bother? Here’s why Codex plus the Amazon...",
      "content": "**NEW** (2026 Jan) Newer versions of Codex uses `/v1/responses` API by default and dropped support for chat completions endpoint. You'll need to add `wire_api = \"responses\"` to your existing config to use the new endpoint instead: <https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html>.\n\n## OpenAI Codex CLI on Amazon Bedrock Models: Why Bother?\n\nHere’s why Codex plus the Amazon Bedrock models make sense under some cases:\n\n1. **Pay as you go**: No fixed cost—just pay for Bedrock tokens and Lambda invocations. No monthly minimum while Amazon Q Developer CLI  has a free-tier quota, you must upgrade to the $19 USD/month paid plan if you have breached it, which *still* enforces usage caps.\n2. **Use your own fine-tuned models**: Swap model endpoints easily; the gateway can even route to your own Amazon Bedrock fine-tunes (e.g. Nova) without friction.\n3. **Transparent logging**: Codex’s request/response logs give you full visibility — a plus for debugging and cost tracking.\n4. **No AWS IAM/Identity required - Perfect for Headless Workloads**: You only need your Bedrock Access Gateway API key; no need to log into your AWS identities with an inconvenient console authentication with your AWS Identity Center user/Builder ID (great for CI/CD and ephemeral cloud instances).\n5. **Regional flexibility**: Yes, you could use [Claude Code with Amazon Bedrock](https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock), but then I live in Hong Kong where Claude model usage is not allowed. \n6. **Amazon Nova Micro: Price King**: For pure simple text LLM tasks, swapping Sonnet 4 for Nova Micro cuts costs by a factor of 85 — Comparing between Nova Micro and Sonnet 4.\n7. If you have a bunch of AWS Credits from AWS events - you're cover with your usages with `gpt-oss` / Nova family of models!\n\n## Setup: Codex CLI + Bedrock Gateway\n\n(UPDATE: Deprecated after Codex v0.80.0 https://github.com/openai/codex/discussions/7782)\n\nGet your Lambda Gateway Function URL and API Key after deployment. (Check my earlier article for a step-by-step guide to get it running on Lambda via AWS SAM: <https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5>)\n\nHere's a no-brainer if you want to skip my article and deploy it right away: \n\n```bash\n(\n  cd /tmp && \\\n  git clone --depth=1 https://github.com/gabrielkoo/bedrock-access-  gateway-function-url && \\\n  cd bedrock-access-gateway-function-url && \\\n  ./prepare_source.sh && \\\n  sam build && \\\n  sam deploy --guided\n)\n```\n\nNow [install Codex](https://github.com/openai/codex?tab=readme-ov-file#installing-and-running-codex-cli):\n\n```bash\nnpm i -g @openai/codex\n```\n\nConfigure Codex like so:\n\n```toml\n# ~/.codex/config.toml\nprofile = 'bedrock'\n\n[profiles.bedrock]\nmodel = 'openai.gpt-oss-120b-1:0'\n# OR\n# model = 'us.amazon.nova-premier-v1:0'\nmodel_provider = 'bedrock'\nmodel_reasoning_effort = \"low\"\n# NEW! Newer versions of Codex uses /v1/responses API by default.\nwire_api = \"chat\"\n\n[model_providers.bedrock]\nname = 'bedrock'\nbase_url = 'https://RANDOM_HASH_HERE.lambda-url.AWS_REGION.on.aws/api/v1'\nenv_key = 'CODEX_OPENAI_API_KEY'\n```\n\nAlternatively, if you want to stick to only `gpt-oss` models but not e.g. Claude/Nova families of models, you can use the latest official OpenAI compatible endpoint with an Amazon Bedrock API Key instead - there will be no need to host the Bedrock Access Gateway:\n\n```toml\n...\nweb_search = \"disabled\"\n\n[model_providers.bedrock]\nname = \"AmazonBedrock\"\nbase_url = \"https://bedrock-mantle.us-west-1.api.aws/v1\"\nenv_key = \"ENV_KEY_FOR_YOUR_BEDROCK_API_KEY\"\n\n...\n\n[profiles.gpt-oss]\n# NOTE: The model ID is truncated if you use the responses API.\nmodel = \"openai.gpt-oss-120b\"\n\n```\n\nQuery the LLM:\n\n```bash\ncodex --profile bedrock \"What is my public IP address?\"\n```\n\n![Codex with Bedrock model in action](/assets/img/f87b08972878.png)\n\n\n## Model Support\n\nNote that not all Bedrock models work over the gateway. Models must support **tool calls**.\n\n**GPT OSS (20b/120b)**: Optimzied with Codex\n**Nova family (Premier, Pro, Lite, Micro):** All tested and working.\n**Claude, Llama, Mistral, Command R:** Working, subject to regional restrictions (e.g. Hong Kong).\n\n## Amazon Q Developer CLI vs Codex CLI on Bedrock\n\nAmazon Q Developer CLI is indeed **officially supported in Hong Kong** — but after your free usage ([50 agentic chats/month](https://aws.amazon.com/q/developer/pricing/)), you'll need the $19/month paid plan, and may hit quotas even then.\n\nCodex CLI via Amazon Bedrock gives *unmetered usage* (subject to whatever quotas you have on Amazon Bedrock itself and Lambda), no AWS login required - you just need to prepare the API key that you defined your self when you deployed the Bedrock Access Gateway.\n\n## Why Don't I Just Use the new OpenAI Compatible Endpoint?\n\nRefer to my other blog article [AWS Launches OpenAI-Compatible API for Bedrock (and I Did Some Tests!)](https://dev.to/aws-builders/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd), the new OpenAI compatible Amazon Bedrock API endpoint supports `gpt-oss` 20b as well as 120b out of the box, other models like Nova or Claude are not supported.\n\nSo with my solution of wrapping the calls via a Bedrock Access Gateway, you can switch to other models whenever you want according your choice.\n\n## Summary\n\nCodex CLI + Amazon Bedrock (via OpenAI-compatible gateway) gives developers a way to use pay-as-you-go agentic CLI Agents, swap fine-tuned models easily, and avoid region/pricing issues present in other AWS or Anthropic toolings. For minimal cost, Nova Micro is unbeatable for text workloads. And yes, my serverless gateway solution is the backbone — but more about that in [my previous blog](https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5)!",
      "excerpts": [
        "**NEW** (2026 Jan) Newer versions of Codex uses `/v1/responses` API by default and dropped support for chat completions endpoint. You'll need to add `wire_api = \"responses\"` to your existing config to use the new endpoint instead: .",
        "OpenAI Codex CLI on Amazon Bedrock Models: Why Bother?",
        "Here’s why Codex plus the Amazon Bedrock models make sense under some cases:",
        "1. **Pay as you go**: No fixed cost—just pay for Bedrock tokens and Lambda invocations. No monthly minimum while Amazon Q Developer CLI has a free-tier quota, you must upgrade to the $19 USD/month paid plan if you have breached it, which *still* enforces usage caps. 2. **Use your own fine-tuned models**: Swap model endpoints easily; the gateway can even route to your own Amazon Bedrock fine-tunes (e.g. Nova) without friction. 3. **Transparent logging**: Codex’s request/response logs give you full visibility — a plus for debugging and cost tracking. 4. **No AWS IAM/Identity required - Perfect for Headless Workloads**: You only need your Bedrock Access Gateway API key; no need to log into your AWS identities with an inconvenient console authentication with your AWS Identity Center user/Builder ID (great for CI/CD and ephemeral cloud instances). 5. **Regional flexibility**: Yes, you could use [Claude Code with Amazon Bedrock](https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock), but then I live in Hong Kong where Claude model usage is not allowed. 6. **Amazon Nova Micro: Price King**: For pure simple text LLM tasks, swapping Sonnet 4 for Nova Micro cuts costs by a factor of ",
        "Setup: Codex CLI + Bedrock Gateway",
        "(UPDATE: Deprecated after Codex v0.80.0 https://github.com/openai/codex/discussions/7782)",
        "Get your Lambda Gateway Function URL and API Key after deployment. (Check my earlier article for a step-by-step guide to get it running on Lambda via AWS SAM: )",
        "Here's a no-brainer if you want to skip my article and deploy it right away:",
        "Now [install Codex](https://github.com/openai/codex?tab=readme-ov-file#installing-and-running-codex-cli):",
        "[profiles.bedrock] model = 'openai.gpt-oss-120b-1:0' OR model = 'us.amazon.nova-premier-v1:0' model_provider = 'bedrock' model_reasoning_effort = \"low\" NEW! Newer versions of Codex uses /v1/responses API by default. wire_api = \"chat\"",
        "[model_providers.bedrock] name = 'bedrock' base_url = 'https://RANDOM_HASH_HERE.lambda-url.AWS_REGION.on.aws/api/v1' env_key = 'CODEX_OPENAI_API_KEY' ```",
        "Alternatively, if you want to stick to only `gpt-oss` models but not e.g. Claude/Nova families of models, you can use the latest official OpenAI compatible endpoint with an Amazon Bedrock API Key instead - there will be no need to host the Bedrock Access Gateway:",
        "[model_providers.bedrock] name = \"AmazonBedrock\" base_url = \"https://bedrock-mantle.us-west-1.api.aws/v1\" env_key = \"ENV_KEY_FOR_YOUR_BEDROCK_API_KEY\"",
        "[profiles.gpt-oss] NOTE: The model ID is truncated if you use the responses API. model = \"openai.gpt-oss-120b\"",
        "![Codex with Bedrock model in action](/assets/img/f87b08972878.png)",
        "Note that not all Bedrock models work over the gateway. Models must support **tool calls**.",
        "**GPT OSS (20b/120b)**: Optimzied with Codex **Nova family (Premier, Pro, Lite, Micro):** All tested and working. **Claude, Llama, Mistral, Command R:** Working, subject to regional restrictions (e.g. Hong Kong).",
        "Amazon Q Developer CLI vs Codex CLI on Bedrock",
        "Amazon Q Developer CLI is indeed **officially supported in Hong Kong** — but after your free usage ([50 agentic chats/month](https://aws.amazon.com/q/developer/pricing/)), you'll need the $19/month paid plan, and may hit quotas even then.",
        "Codex CLI via Amazon Bedrock gives *unmetered usage* (subject to whatever quotas you have on Amazon Bedrock itself and Lambda), no AWS login required - you just need to prepare the API key that you defined your self when you deployed the Bedrock Access Gateway.",
        "Why Don't I Just Use the new OpenAI Compatible Endpoint?",
        "Refer to my other blog article [AWS Launches OpenAI-Compatible API for Bedrock (and I Did Some Tests!)](https://dev.to/aws-builders/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd), the new OpenAI compatible Amazon Bedrock API endpoint supports `gpt-oss` 20b as well as 120b out of the box, other models like Nova or Claude are not supported.",
        "So with my solution of wrapping the calls via a Bedrock Access Gateway, you can switch to other models whenever you want according your choice.",
        "Codex CLI + Amazon Bedrock (via OpenAI-compatible gateway) gives developers a way to use pay-as-you-go agentic CLI Agents, swap fine-tuned models easily, and avoid region/pricing issues present in other AWS or Anthropic toolings. For minimal cost, Nova Micro is unbeatable for text workloads. And yes, my serverless gateway solution is the backbone — but more about that in [my previous blog](https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5)!"
      ]
    },
    {
      "id": "article:aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd",
      "source_type": "article",
      "title": "🚀 AWS Launches OpenAI-Compatible API for Bedrock (and I Did Some Tests!)",
      "url": "https://gabrielkoo.com/blog/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd/",
      "canonical_url": "https://dev.to/aws-builders/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd",
      "published_at": "2025-08-07",
      "last_verified_at": "2026-08-23",
      "tags": [
        "bedrock",
        "aws",
        "openai"
      ],
      "description": "🚀 AWS OpenAI-Compatible API for Bedrock OSS GPT: Real-World Dev Tests & Insights AWS...",
      "content": "## 🚀 AWS OpenAI-Compatible API for Bedrock OSS GPT: Real-World Dev Tests & Insights\n\nAWS just dropped a [bombshell](https://aws.amazon.com/blogs/aws/openai-open-weight-models-now-available-on-aws/) by launching open-weight GPT models (`gpt-oss-120b`, `gpt-oss-20b`) on Amazon Bedrock as serverless, pay-as-you-go option to self hosting — this caught a lot of headlines 👀.\n\nBut as someone obsessed with **Developer Experience (DX)**, I was even more stoked that AWS **finally** launched [an official OpenAI-compatible endpoint](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html):  \n\n`https://bedrock-runtime.us-west-2.amazonaws.com/openai/v1`  \n\nThis puts AWS right alongside Gemini/VertexAI and Anthropic as companies providing first-party OpenAI SDK compatibility, and finally brings native OpenAI \"plug-and-play\" infra to AWS 🤩\n\nBefore we dive in, **here’s my previous deep-dive blog for context on using Amazon Bedrock models with OpenAI compatibility (without fixed costs):**\n  \n👉 <https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5>\n\n---\n\n## 😎 Wait—Does This Make the Bedrock Proxy Gateway Projects Stale?\n\nNope! While simple OpenAI API calls still runs perfectly with these new official compatibile Amazon Bedrock Runtimeendpoints, **there are critical compatibility gaps** — so custom proxies/gateways like my [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url) project and the official [bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway/) project are nowhere near obsolete yet.\n\n---\n\n## By the Way, AWS Credits Does Cover the `gpt-oss` models!\n\nIt has been quite a pity that typical AWS Credits do not cover other models like Claude or Mistral. It's great that this time with the official partnership between AWS and OpenAI, the usage for `gpt-oss` models seems covered finally:\n\n![Image description](/assets/img/23f1c54741f8.png)\n\n---\n\n## 🧑‍💻 I Vibe-Coded the Tests with Kiro AI IDE — Here’s What Happened\n\nYou can check my **actual repo and test scripts here:**  \n📦 <https://github.com/gabrielkoo/test-official-amazon-bedrock-openai-compatible-endpoint>\n\n**AWS Official Docs:** [AWS Bedrock Docs: OpenAI Chat Completions API](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html)  \n*(Note: The docs didn’t mention support is limited to latest OpenAI `gpt-oss` models only, and didn't mention directly support for `tool_call` or `response_format` features/parameters, so do read on for my real-world dev findings!)*\n\n## Summary Table:\n\nAs a control test, I also ran the same testing script with my local Ollama `gpt-oss:20b` model:\n\n| Test                                                 | Bedrock OpenAI Endpoint                  | Local Ollama + `gpt-oss`                | Notes                                                                                      |\n|------------------------------------------------------|------------------------------------------|---------------------------------------|--------------------------------------------------------------------------------------------|\n| Basic Chat Completion + Reasoning                    | ✅ Works: Step-wise reasoning included   | ✅ Works, chain-of-thought            | Bedrock wraps reasoning in `<reasoning>` tags, both are great for math/logic explanations   |\n| `reasoning_effort` parameter                         | ✅ Supported, adjusts explanation depth  | ✅ Supported, works                    | Fine-tune output detail—rare outside OpenAI OSS models                                      |\n| 🔧 Tool/Function Calling (`tools`/`tool_choice`)     | ❌ Only intent as JSON, *not* native `tool_calls` | ✅ True OpenAI tool_call schema      | Just like the initial state of `o1-mini`: AWS \"gets\" the tools, but you can’t delegate natively |\n| `response_format` (JSON schema)                      | ❌ Not supported, API error              | ✅ Works, emits JSON                   | Bedrock doesn’t do response schema yet                                                     |\n| Token param (`max_tokens` vs `max_completion_tokens`) | ❌ Must use `max_completion_tokens`      | ✅ Accepts either                      | Bedrock is stricter, adjust scripts accordingly                                            |\n| Non-OSS Model (`Nova`, `Titan`, etc.)                | ✅ \"Model not found\", as expected         | ✅ Same                                | Endpoint only exposes OSS GPT models (`gpt-oss-*`) for now                                 |\n\n---\n\n## 🌟 Key DX Takeaways\n\n- **API Parity, But Not Quite Full Compatibility:**  \n  Any OpenAI SDK code (and most OpenAI agent frameworks) can now point at AWS’s Bedrock Runtime `/openai/v1` endpoint with just env var tweaks — no code rewriting. This UX is 🔥 for engineers and innovation teams!\n- **Transparent, Adjustable Reasoning:**  \n  Get full chain-of-thought out-of-the-box, plus parametric reasoning level control. Perfect for math, science, coding, and explainability applications!\n- **Tool Calling Support—Almost There:**  \n  AWS Bedrock OSS GPT models *recognize* function call intent (return JSON), but don’t yet emit proper `tool_calls` blocks in the response. (While the same test case passed for my local Ollama test on the same `gpt-oss` model). This feels just like the early days of the `o1-mini` models, so: expect better in future, but for now, your OpenAI tool-calling apps will need adapters or proxies. 🙂\n- **Strict OpenAI Param Parsing:**  \n  If your code uses `max_tokens`, it’ll break here — swap to `max_completion_tokens` for as `gpt-oss` is a reasoning model.\n- **Model Registry/Open Weight Only:**  \n  The official endpoint supports only `gpt-oss` models so far (`gpt-oss-120b`, `gpt-oss-20b`). Other Amazon Bedrock serverless models like `Nova` will get you a fast fail as of now.\n- **Still Need Dev Proxy Power:**  \n  [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url) is *not* obsolete! It remains highly useful for compatibility workarounds or for non-`gpt-oss` models.\n\nAs a disclaimer, if we refer to the model documentation of `gpt-oss` on AWS, <https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-openai.html>, tool calls were not supported directly for `gpt-oss` on Amazon Bedrock hosted version as of now, so it’s totally possible when AWS release more model support for the new OpenAI endpoint, these could no longer become a concern!\n\n---\n\n## 💡 DX Tips for Devs\n\n- **Add backward/forward compatibility adapters** if you’re building serious agentic stacks.\n- **Watch the docs** ([Invoke a model with the OpenAI Chat Completions API](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html)), but always test yourself — AWS hasn't listed all the current limitations yet clearly in the documentation!\n- **Port your scripts with care:**  \n  - Reasoning, logic, coding, and OpenAI-style chat: 👍  \n  - Tool use, output schemas: ⚠️ adapter or proxy needed today!\n- **OSS all the things!** Making the full open-weight LLM stack *API-portable* is a big win for multi-cloud, local/cloud hybrid, and plain old sovereignty nerds. It feels like a new era for \"bring your own infra, but keep your SDKs.\"\n\n---\n\n## Closing\n\nThanks to [Kiro AI IDE](https://kiro.dev/) for the vibe-code collab and to AWS's team on making the OpenAI compatible endpoint finally launched!\n\nAWS’s new official OpenAI compatible endpoint for `gpt-oss` models is a major leap for cloud/dev portability, even if a few “classic” OpenAI features like native function calling aren’t fully there (yet). If you need adapters, or want total control, proxies like mine remain essential. But for standard chat/reasoning patterns, Bedrock is now OpenAI SDK ready — from your own existing code based on OpenAI SDKs.\n\nHappy building! 🚀🤖\n\nP.S. This article was written on 2025-08-06, 1 day after the new endpoint it was launched, so it's totally possible that by the time you read this blog article, AWS could have already rolled out more support for other common OpenAI SDK features!",
      "excerpts": [
        "🚀 AWS OpenAI-Compatible API for Bedrock OSS GPT: Real-World Dev Tests & Insights",
        "AWS just dropped a [bombshell](https://aws.amazon.com/blogs/aws/openai-open-weight-models-now-available-on-aws/) by launching open-weight GPT models (`gpt-oss-120b`, `gpt-oss-20b`) on Amazon Bedrock as serverless, pay-as-you-go option to self hosting — this caught a lot of headlines 👀.",
        "But as someone obsessed with **Developer Experience (DX)**, I was even more stoked that AWS **finally** launched [an official OpenAI-compatible endpoint](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html):",
        "`https://bedrock-runtime.us-west-2.amazonaws.com/openai/v1`",
        "This puts AWS right alongside Gemini/VertexAI and Anthropic as companies providing first-party OpenAI SDK compatibility, and finally brings native OpenAI \"plug-and-play\" infra to AWS 🤩",
        "Before we dive in, **here’s my previous deep-dive blog for context on using Amazon Bedrock models with OpenAI compatibility (without fixed costs):** 👉",
        "😎 Wait—Does This Make the Bedrock Proxy Gateway Projects Stale?",
        "Nope! While simple OpenAI API calls still runs perfectly with these new official compatibile Amazon Bedrock Runtimeendpoints, **there are critical compatibility gaps** — so custom proxies/gateways like my [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url) project and the official [bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway/) project are nowhere near obsolete yet.",
        "By the Way, AWS Credits Does Cover the `gpt-oss` models!",
        "It has been quite a pity that typical AWS Credits do not cover other models like Claude or Mistral. It's great that this time with the official partnership between AWS and OpenAI, the usage for `gpt-oss` models seems covered finally:",
        "![Image description](/assets/img/23f1c54741f8.png)",
        "🧑‍💻 I Vibe-Coded the Tests with Kiro AI IDE — Here’s What Happened",
        "You can check my **actual repo and test scripts here:** 📦",
        "**AWS Official Docs:** [AWS Bedrock Docs: OpenAI Chat Completions API](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html) *(Note: The docs didn’t mention support is limited to latest OpenAI `gpt-oss` models only, and didn't mention directly support for `tool_call` or `response_format` features/parameters, so do read on for my real-world dev findings!)*",
        "As a control test, I also ran the same testing script with my local Ollama `gpt-oss:20b` model:",
        "| Test | Bedrock OpenAI Endpoint | Local Ollama + `gpt-oss` | Notes | |------------------------------------------------------|------------------------------------------|---------------------------------------|--------------------------------------------------------------------------------------------| | Basic Chat Completion + Reasoning | ✅ Works: Step-wise reasoning included | ✅ Works, chain-of-thought | Bedrock wraps reasoning in ` ` tags, both are great for math/logic explanations | | `reasoning_effort` parameter | ✅ Supported, adjusts explanation depth | ✅ Supported, works | Fine-tune output detail—rare outside OpenAI OSS models | | 🔧 Tool/Function Calling (`tools`/`tool_choice`) | ❌ Only intent as JSON, *not* native `tool_calls` | ✅ True OpenAI tool_call schema | Just like the initial state of `o1-mini`: AWS \"gets\" the tools, but you can’t delegate natively | | `response_format` (JSON schema) | ❌ Not supported, API error | ✅ Works, emits JSON | Bedrock doesn’t do response schema yet | | Token param (`max_tokens` vs `max_completion_tokens`) | ❌ Must use `max_completion_tokens` | ✅ Accepts either | Bedrock is stricter, adjust scripts accordingly | | Non-OSS Model (`Nova`, `Titan",
        "- **API Parity, But Not Quite Full Compatibility:** Any OpenAI SDK code (and most OpenAI agent frameworks) can now point at AWS’s Bedrock Runtime `/openai/v1` endpoint with just env var tweaks — no code rewriting. This UX is 🔥 for engineers and innovation teams! - **Transparent, Adjustable Reasoning:** Get full chain-of-thought out-of-the-box, plus parametric reasoning level control. Perfect for math, science, coding, and explainability applications! - **Tool Calling Support—Almost There:** AWS Bedrock OSS GPT models *recognize* function call intent (return JSON), but don’t yet emit proper `tool_calls` blocks in the response. (While the same test case passed for my local Ollama test on the same `gpt-oss` model). This feels just like the early days of the `o1-mini` models, so: expect better in future, but for now, your OpenAI tool-calling apps will need adapters or proxies. 🙂 - **Strict OpenAI Param Parsing:** If your code uses `max_tokens`, it’ll break here — swap to `max_completion_tokens` for as `gpt-oss` is a reasoning model. - **Model Registry/Open Weight Only:** The official endpoint supports only `gpt-oss` models so far (`gpt-oss-120b`, `gpt-oss-20b`). Other Amazon Bedrock se",
        "As a disclaimer, if we refer to the model documentation of `gpt-oss` on AWS, , tool calls were not supported directly for `gpt-oss` on Amazon Bedrock hosted version as of now, so it’s totally possible when AWS release more model support for the new OpenAI endpoint, these could no longer become a concern!",
        "- **Add backward/forward compatibility adapters** if you’re building serious agentic stacks. - **Watch the docs** ([Invoke a model with the OpenAI Chat Completions API](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html)), but always test yourself — AWS hasn't listed all the current limitations yet clearly in the documentation! - **Port your scripts with care:** - Reasoning, logic, coding, and OpenAI-style chat: 👍 - Tool use, output schemas: ⚠️ adapter or proxy needed today! - **OSS all the things!** Making the full open-weight LLM stack *API-portable* is a big win for multi-cloud, local/cloud hybrid, and plain old sovereignty nerds. It feels like a new era for \"bring your own infra, but keep your SDKs.\"",
        "Thanks to [Kiro AI IDE](https://kiro.dev/) for the vibe-code collab and to AWS's team on making the OpenAI compatible endpoint finally launched!",
        "AWS’s new official OpenAI compatible endpoint for `gpt-oss` models is a major leap for cloud/dev portability, even if a few “classic” OpenAI features like native function calling aren’t fully there (yet). If you need adapters, or want total control, proxies like mine remain essential. But for standard chat/reasoning patterns, Bedrock is now OpenAI SDK ready — from your own existing code based on OpenAI SDKs.",
        "P.S. This article was written on 2025-08-06, 1 day after the new endpoint it was launched, so it's totally possible that by the time you read this blog article, AWS could have already rolled out more support for other common OpenAI SDK features!"
      ]
    },
    {
      "id": "article:regain-access-to-amazon-q-cli-in-cloudshell-with-this-simple-trick-58m3",
      "source_type": "article",
      "title": "Regain access to Amazon Q CLI in CloudShell with This Simple Trick",
      "url": "https://gabrielkoo.com/blog/regain-access-to-amazon-q-cli-in-cloudshell-with-this-simple-trick-58m3/",
      "canonical_url": "https://dev.to/aws-builders/regain-access-to-amazon-q-cli-in-cloudshell-with-this-simple-trick-58m3",
      "published_at": "2025-08-03",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "cloud",
        "amazonqcli"
      ],
      "description": "If you’ve tried to use the Amazon Q CLI a.k.a. q chat command in AWS CloudShell recently, you might...",
      "content": "If you’ve tried to use the Amazon Q CLI a.k.a. `q chat` command in **AWS CloudShell** recently, you might have seen the frustrating message 🤯:\n\n> _Q CLI integration is temporarily disabled. For continued access to Q Chat, please use the integrated version in your AWS Console._\n\nThis is annoying especially for me as a true lover of Amazon Q CLI which is so far the best GenAI CLI agent for AWS daily users.\n\nThe good news is here’s a guide on how to force reinstall Amazon Q CLI and log in with your own account. Plus, some perspective on why this happens, and why Amazon Q CLI is still a must-have for me 🛠️ — especially if you’re experimenting with GenAI in cloud environments.\n\n## What’s Happening?\n\nAs of time of writing (2025-08-02), AWS has temporarily disabled Amazon Q chat CLI features in CloudShell due to an internal issue (officially documented by AWS [here](https://docs.aws.amazon.com/cloudshell/latest/userguide/q-cli-features-in-cloudshell.html)). Q-based inline suggestions and the actual chat functionality are both disabled in CloudShell. ⛔️\n\n## Why Is This an Issue (My Own Speculation)?\n\nI saw an user reporting the issue ⚠️ as early as 2025-05-23: <https://github.com/aws/amazon-q-developer-cli/issues/1944>.\n\nHere’s why I think it’s happening 🧠:\n\n**CloudShell** authenticates you for AWS credentials natively using **AWS IAM users, roles, or Identity Center Access Role sessions** — so that you can do API calls like `aws sts get-caller-identity` within CloudShell without the hassle of configuring the credentials.\n\nIn contrast, **Amazon Q CLI** expects authentication via **AWS Builder ID** or **AWS IAM Identity Center** for Pro plan.\n\nThese authentication models may not be exactly compatible, especially for chat-based workflows requiring licensing or entitlements outside basic AWS IAM integration.\n\nOne last possibility is that it is actually quite hard to apply limits or attribute usage rates of Amazon Q CLI. Imagine if one’s Amazon Q CLI usage with a particular IAM user in CloudShell session exceeds a fair pre-defined limit, one can just create another IAM user and have rate limits reset as a separate user. One could even programmatically set this up for literally unlimited Amazon Q CLI usage quota (if there is not any AWS Account wide limit applied). 💸\n\nUntil AWS aligns these behind the scenes, expect more time from the Amazon Q CLI team to work on a fix.\n\n## But Amazon Q CLI is Still Cool!\n\nEven with this hiccup, Amazon Q CLI remains hugely valuable ⚡️:\n\n- AI-driven CLI suggestions right where you work\n- Natural language command generation\n- Instant code explanations and diagnostics\n- Makes security and automation work faster and less error-prone\n\nIf you use it outside CloudShell, it truly supercharges your workflow.\n\n## Force Reinstall & Login Steps\n\nHere’s comes to the main topic - how you can force a clean install and log in outside CloudShell.\n\nEasy. Since AWS CloudShell computing environment is just x64 Amazon Linux under the hood, just force uninstall it and then install it according to the [official documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing-ssh-setup-autocomplete.html#command-line-download-install-file).\n\n```bash\n# This removes the existing installation\nsudo rm `which q`\n# Download the latest ZIP-based installer\ncurl --proto '=https' --tlsv1.2 -sSf \"https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux-musl.zip\" -o \"q.zip\"\n\nunzip q.zip\n./q/install.sh\n```\n\nWhen prompted:\n\n> Do you want q to modify your shell config (you will have to manually do this otherwise)?\nAnswer: **Yes**\n\nThis helps set up the **Amazon Q CLI** and autocompletion back.\n\nNext, authenticate with your preferred credentials:\n\n```bash\nq login\n```\n\nYou’ll be guided through a browser-based process (powered by Builder ID, Identity Center, etc.), enter the code, and your CLI session will be authenticated. Once authorized, you get full access (outside limitations of CloudShell).\n\n![Image description](/assets/img/ea59f7ecf7b4.jpeg)\n\nNow, let’s test it out. Try:\n\n```bash\nq help\nq inline enable\n```\n\nWith proper authentication, you’ll see **Amazon Q CLI** in all its glory (in supported environments) 🚀.\n\n![Image description](/assets/img/5979c6e0bf80.jpeg)\n\n## Important Points to Note:\n\n- With my method, your Amazon Q CLI usages in CloudShell are bind to your own AWS Builder ID account / your Identity Center user. Be aware of usage rate limits. 🧾\n- AWS CloudShell environments would be automatically deleted after **120 days of inactivity**. You might need to re-do this hack if you haven’t used a particular CloudShell environment for more than 120 days. 🗑️\n\n## Key Takeaways\n\n- Don’t stress if `q chat` is broken in CloudShell—it’s a temporary AWS-side limitation.\n- Use my way to force reinstall and login outside **AWS CloudShell** for a working setup.\n- Integration hurdles are likely caused by the difference in authentication flows: IAM / Identity Center built into CloudShell vs. Builder ID or Pro Plan requirements for Amazon Q CLI.\n- Amazon Q CLI is a great tool—just leverage it where AWS supports full features, and watch for future CloudShell improvements.\n\nHappy hacking — enjoy more intelligent, secure, and productive time in your CloudShell terminal! ☁️",
      "excerpts": [
        "If you’ve tried to use the Amazon Q CLI a.k.a. `q chat` command in **AWS CloudShell** recently, you might have seen the frustrating message 🤯:",
        "> _Q CLI integration is temporarily disabled. For continued access to Q Chat, please use the integrated version in your AWS Console._",
        "This is annoying especially for me as a true lover of Amazon Q CLI which is so far the best GenAI CLI agent for AWS daily users.",
        "The good news is here’s a guide on how to force reinstall Amazon Q CLI and log in with your own account. Plus, some perspective on why this happens, and why Amazon Q CLI is still a must-have for me 🛠️ — especially if you’re experimenting with GenAI in cloud environments.",
        "As of time of writing (2025-08-02), AWS has temporarily disabled Amazon Q chat CLI features in CloudShell due to an internal issue (officially documented by AWS [here](https://docs.aws.amazon.com/cloudshell/latest/userguide/q-cli-features-in-cloudshell.html)). Q-based inline suggestions and the actual chat functionality are both disabled in CloudShell. ⛔️",
        "Why Is This an Issue (My Own Speculation)?",
        "I saw an user reporting the issue ⚠️ as early as 2025-05-23: .",
        "Here’s why I think it’s happening 🧠:",
        "**CloudShell** authenticates you for AWS credentials natively using **AWS IAM users, roles, or Identity Center Access Role sessions** — so that you can do API calls like `aws sts get-caller-identity` within CloudShell without the hassle of configuring the credentials.",
        "In contrast, **Amazon Q CLI** expects authentication via **AWS Builder ID** or **AWS IAM Identity Center** for Pro plan.",
        "These authentication models may not be exactly compatible, especially for chat-based workflows requiring licensing or entitlements outside basic AWS IAM integration.",
        "One last possibility is that it is actually quite hard to apply limits or attribute usage rates of Amazon Q CLI. Imagine if one’s Amazon Q CLI usage with a particular IAM user in CloudShell session exceeds a fair pre-defined limit, one can just create another IAM user and have rate limits reset as a separate user. One could even programmatically set this up for literally unlimited Amazon Q CLI usage quota (if there is not any AWS Account wide limit applied). 💸",
        "Until AWS aligns these behind the scenes, expect more time from the Amazon Q CLI team to work on a fix.",
        "But Amazon Q CLI is Still Cool!",
        "Even with this hiccup, Amazon Q CLI remains hugely valuable ⚡️:",
        "- AI-driven CLI suggestions right where you work - Natural language command generation - Instant code explanations and diagnostics - Makes security and automation work faster and less error-prone",
        "If you use it outside CloudShell, it truly supercharges your workflow.",
        "Here’s comes to the main topic - how you can force a clean install and log in outside CloudShell.",
        "Easy. Since AWS CloudShell computing environment is just x64 Amazon Linux under the hood, just force uninstall it and then install it according to the [official documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing-ssh-setup-autocomplete.html#command-line-download-install-file).",
        "unzip q.zip ./q/install.sh ```",
        "> Do you want q to modify your shell config (you will have to manually do this otherwise)? Answer: **Yes**",
        "This helps set up the **Amazon Q CLI** and autocompletion back.",
        "Next, authenticate with your preferred credentials:",
        "You’ll be guided through a browser-based process (powered by Builder ID, Identity Center, etc.), enter the code, and your CLI session will be authenticated. Once authorized, you get full access (outside limitations of CloudShell).",
        "![Image description](/assets/img/ea59f7ecf7b4.jpeg)",
        "With proper authentication, you’ll see **Amazon Q CLI** in all its glory (in supported environments) 🚀.",
        "![Image description](/assets/img/5979c6e0bf80.jpeg)",
        "- With my method, your Amazon Q CLI usages in CloudShell are bind to your own AWS Builder ID account / your Identity Center user. Be aware of usage rate limits. 🧾 - AWS CloudShell environments would be automatically deleted after **120 days of inactivity**. You might need to re-do this hack if you haven’t used a particular CloudShell environment for more than 120 days. 🗑️",
        "- Don’t stress if `q chat` is broken in CloudShell—it’s a temporary AWS-side limitation. - Use my way to force reinstall and login outside **AWS CloudShell** for a working setup. - Integration hurdles are likely caused by the difference in authentication flows: IAM / Identity Center built into CloudShell vs. Builder ID or Pro Plan requirements for Amazon Q CLI. - Amazon Q CLI is a great tool—just leverage it where AWS supports full features, and watch for future CloudShell improvements.",
        "Happy hacking — enjoy more intelligent, secure, and productive time in your CloudShell terminal! ☁️"
      ]
    },
    {
      "id": "article:building-github-style-contribution-grids-for-devto-articles-with-ai-3fpn",
      "source_type": "article",
      "title": "Building GitHub-Style Contribution Grids for dev.to Articles with Kiro AI IDE",
      "url": "https://gabrielkoo.com/blog/building-github-style-contribution-grids-for-devto-articles-with-ai-3fpn/",
      "canonical_url": "https://dev.to/kirodotdev/building-github-style-contribution-grids-for-devto-articles-with-ai-3fpn",
      "published_at": "2025-08-02",
      "last_verified_at": "2026-08-23",
      "tags": [
        "devto",
        "kirodev",
        "seo",
        "analytics"
      ],
      "description": "How I created beautiful GitHub-style contribution graphs and traffic analytics for my dev.to articles...",
      "content": "*How I created beautiful GitHub-style contribution graphs and traffic analytics for my dev.to articles using Kiro.dev - without writing much code myself*\n\n![Image description](/assets/img/5a9d039e9765.png)\n\n---\n\n## The Problem: Understanding Your Content's Impact\n\nAs a technical writer publishing on dev.to, I found myself constantly wondering about my articles' performance. Sure, dev.to provides basic stats, but I wanted something more comprehensive - something that could show me:\n\n- **GitHub-style contribution grids** for my writing activity\n- **Traffic source analysis** to understand where readers discover my content\n- **Historical trends** to see how articles perform over time\n- **Beautiful visualizations** I could embed anywhere\n\nWorking frequently with SEO teams, I was particularly interested in understanding traffic sources. Were my articles getting discovered through Google searches? LinkedIn shares? Twitter threads? Newsletter mentions? This data would be crucial for optimizing my content distribution strategy.\n\n## The Solution: GitHub-Style Analytics with Traffic Insights\n\nWhat started as a simple idea turned into a comprehensive analytics system that fetches data from dev.to's API and generates beautiful SVG visualizations. The system creates GitHub-style contribution grids and tracks:\n\n### 📊 Core Metrics\n- **Views**: Daily article view counts\n- **Reactions**: Likes, unicorns, and bookmarks\n- **Comments**: Reader engagement levels\n- **Combined Activity**: Weighted scoring system\n\n### 🌐 Traffic Source Analysis\nThe system tracks where your readers come from and visualizes it in a beautiful pie chart:\n- **Direct traffic** (users typing your URL directly)\n- **Search engines** (Google, Bing, DuckDuckGo)\n- **Social media** (LinkedIn, Twitter, Facebook)\n- **Developer platforms** (GitHub, dev.to internal traffic)\n- **Newsletters and blogs** (Substack, personal blogs)\n- **Professional tools** (Slack, Microsoft Office)\n\n![Traffic sources pie chart showing distribution of 19,096 total views](/assets/img/2bad2a13bc04.png)\n\n### 📈 GitHub-Style Contribution Grids\nThe system generates six types of beautiful SVG visualizations:\n\n1. **Views Activity Grid** - GitHub-style contribution graph with green color scheme\n2. **Reactions Activity Grid** - Purple-themed grid showing engagement patterns  \n3. **Combined Activity Grid** - Orange-themed weighted activity visualization\n4. **Top Articles by Views** - Horizontal bar chart of most-viewed content\n5. **Top Articles by Reactions** - Bar chart highlighting most engaging articles\n6. **Traffic Sources Pie Chart** - Visual breakdown of where your readers come from\n\n## The Magic: Built with Kiro.dev\n\nHere's where the story gets interesting - **I didn't write most of this code myself**. The entire project was created using [Kiro.dev](https://kiro.dev/), an AI-powered development environment that acts as your coding partner.\n\n### How Kiro.dev Transformed My Workflow\n\nInstead of spending weeks researching APIs, designing data structures, and debugging visualization code, I simply described what I wanted:\n\n> \"I want to create GitHub-style contribution grids for my dev.to articles, track traffic sources, and generate beautiful SVG visualizations I can embed anywhere.\"\n\nKiro.dev understood the requirements and generated the complete system:\n\n1. **Data fetching** - [`fetch_stats.py`](fetch_stats.py) handles API integration with error handling and rate limiting\n2. **Contribution grids** - [`generate_advanced_graph.py`](generate_advanced_graph.py) creates GitHub-style activity visualizations\n3. **Top articles charts** - [`generate_top_articles.py`](generate_top_articles.py) generates ranking visualizations\n4. **Traffic analysis** - [`generate_traffic_pie_chart.py`](generate_traffic_pie_chart.py) creates traffic source breakdowns\n5. **Automation** - [`.github/workflows/`](.github/workflows/) sets up daily updates\n6. **Data management** - Smart incremental updates and referrer tracking\n\n### The Development Experience\n\nWorking with Kiro.dev felt like pair programming with an expert developer who:\n- **Understood context** - Grasped the full project scope from minimal descriptions\n- **Made smart decisions** - Chose appropriate data structures and algorithms\n- **Wrote clean code** - Generated well-documented, maintainable Python scripts\n- **Anticipated needs** - Added features I hadn't even thought of, like referrer tracking\n\nThe AI didn't just write code - it architected a complete solution.\n\n## Technical Deep Dive\n\n### Data Architecture\n\nThe system organizes data efficiently:\n\n```\ndata/\n├── articles/           # Individual article analytics\n│   ├── {id}-{slug}.json\n├── account.json        # Aggregated account stats\n└── top_articles.json   # Rankings by metrics\n```\n\nEach article file contains:\n- Basic metrics (views, comments, reactions)\n- Daily breakdown of activity\n- **Referrer data** showing traffic sources\n- Metadata for URL construction\n\n### API Integration\n\nThe system uses three dev.to API endpoints:\n- `/analytics/historical` - Historical analytics data\n- `/analytics/referrers` - Traffic source information\n- `/articles/me/published` - Article listings\n\n### Smart Updates\n\nThe fetcher implements intelligent incremental updates:\n- Only fetches new data since last update\n- Refreshes the second-to-last day to catch delayed analytics\n- Handles API rate limits gracefully\n- Maintains data integrity with backup strategies\n\n### Visualization Engine\n\nThe SVG generation system creates publication-ready graphics:\n- **Responsive design** that works at any size\n- **Interactive tooltips** with detailed information\n- **Clickable elements** linking to original articles\n- **Multiple color schemes** for different contexts\n\n## Key Insights from Traffic Analysis\n\nAfter implementing this system, I discovered fascinating patterns in my content's reach:\n\n### Traffic Source Breakdown\nFrom my 19,096 total views, the top 10 referrers account for 14,974 views (78.4%):\n\n1. **Direct traffic (41.6%)** - 7,939 views from users directly accessing articles\n2. **Google.com (24.3%)** - 4,639 views from organic search\n3. **t.co (3.2%)** - 619 views from Twitter links\n4. **LinkedIn.com (3.0%)** - 582 views from professional network shares\n5. **dev.to (2.4%)** - 459 views from platform internal discovery\n6. **office.net (0.9%)** - 172 views from Microsoft Office applications\n7. **duckduckgo.com (0.9%)** - 172 views from privacy-focused search\n8. **bing.com (0.8%)** - 158 views from Microsoft search\n9. **facebook.com (0.6%)** - 121 views from social media shares\n10. **mrugalski.pl (0.6%)** - 113 views from a personal blog referral\n\n### SEO Team Goldmine\n\nThis data proved invaluable for SEO strategy discussions:\n- **Search dominance** - Google alone drives 24.3% of traffic, with additional search engines contributing more\n- **Social amplification** - Twitter (3.2%) and LinkedIn (3.0%) show strong professional sharing\n- **Platform dynamics** - Only 2.4% comes from dev.to's internal discovery, showing external reach\n- **Direct engagement** - 41.6% direct traffic indicates strong brand recognition and bookmarking\n\n### Content Performance Patterns\n\nThe top-performing article \"I bought us-east-1.com\" generated 9,021 views with fascinating traffic patterns:\n- **71% direct traffic** - Indicating strong word-of-mouth sharing\n- **6.6% Twitter traffic** - Viral social media spread\n- **5.7% LinkedIn traffic** - Professional network engagement\n\n## Showcasing Results: Raw GitHub Content Integration\n\nOne of the most powerful features is the ability to embed these visualizations anywhere using GitHub's raw content URLs. Here's how:\n\n### Step 1: Generate Your Visualizations\n```bash\n# GitHub-style contribution grids\npython3 generate_advanced_graph.py --metric views --color github\npython3 generate_advanced_graph.py --metric reactions --color purple\n\n# Top articles charts  \npython3 generate_top_articles.py --metric reactions --count 3\n\n# Traffic sources analysis\npython3 generate_traffic_pie_chart.py\n```\n\n### Step 2: Embed in README Files\n```markdown\n![Views Activity](/assets/img/1b55399b9f84.svg)\n![Top Articles](/assets/img/81a2e8aa0cee.svg)\n![Traffic Sources](/assets/img/e7a039bc0214.svg)\n```\n\n### Step 3: Use in Documentation\nThe SVGs work perfectly in:\n- **GitHub README files** - Showcase your writing activity\n- **Portfolio websites** - Demonstrate content creation consistency\n- **Blog posts** - Visual proof of engagement metrics\n- **Social media** - Eye-catching performance summaries\n\n![README file showing embedded SVG graphs](/assets/img/4724e2869be9.png)\n\n### Pro Tips for Display\n\n1. **Repository structure** - Keep graphs in a dedicated `/graphs` folder\n2. **Naming convention** - Use descriptive filenames like `devto_views_graph.svg`\n3. **Update automation** - GitHub Actions ensures graphs stay current\n4. **Multiple formats** - Generate different metrics and color schemes for various contexts\n\n## The Automation Layer\n\nGitHub Actions keeps everything current with daily updates:\n\n```yaml\n# Runs daily at midnight UTC\n# Fetches latest analytics\n# Generates updated visualizations\n# Commits changes automatically\n```\n\nThis means your analytics dashboard stays fresh without any manual intervention.\n\n## Lessons Learned\n\n### 1. AI-Assisted Development is Transformative\nKiro.dev didn't just speed up development - it elevated the entire solution. The AI suggested features and optimizations I wouldn't have considered, like:\n- Logarithmic scaling for better visualization distribution\n- Referrer data aggregation for traffic source analysis\n- Incremental update strategies for API efficiency\n\n### 2. Data Visualization Drives Insights\nHaving visual representations of my writing activity revealed patterns invisible in raw numbers:\n- Consistency gaps in publishing schedule\n- Correlation between article topics and engagement\n- Traffic source diversity indicating content reach\n\n### 3. SEO Integration is Crucial\nThe referrer tracking proved invaluable for SEO discussions:\n- Identifying high-performing traffic sources\n- Understanding content discovery patterns\n- Optimizing distribution strategies\n\n## Getting Started\n\nWant to build your own analytics dashboard? The complete system is available as an open-source project. Here's how to get started:\n\n### Quick Setup\n1. Fork the repository - \n**[📊 devto-stats-github-action](https://github.com/gabrielkoo/devto-stats-github-action)**\n2. Add your dev.to API key to GitHub Actions Secrets\n3. Enable GitHub Actions for automatic updates\n4. Customize visualizations to match your brand\n\n### Customization Options\n- **Color schemes** - GitHub green, purple, blue, or orange themes\n- **Metrics focus** - Emphasize views, reactions, or combined activity\n- **Time ranges** - Adjust historical data collection periods\n- **Graph types** - Modify visualization styles and layouts\n\n## Conclusion\n\nBuilding this analytics dashboard taught me that the future of development isn't about replacing developers - it's about amplifying our capabilities. With Kiro.dev as my coding partner, I created a sophisticated system that would have taken weeks to build manually.\n\nThe insights gained from traffic source analysis have already improved my content strategy, and the beautiful visualizations provide compelling evidence of writing consistency and engagement.\n\nMost importantly, this project demonstrates how AI-assisted development can help creators focus on what matters most - creating great content - while still building the tools needed to understand and optimize their impact.\n\nWhether you're a technical writer, developer advocate, or content creator, having detailed analytics about your work's reach and impact is invaluable. And with tools like Kiro.dev, building these systems is more accessible than ever.\n\n---\n\n*Ready to build your own analytics dashboard? Check out the complete source code and start tracking your content's impact today.*\n\n\n**[📊 devto-stats-github-action](https://github.com/gabrielkoo/devto-stats-github-action)**\n\n![Sample](/assets/img/1151c14ece05.png)",
      "excerpts": [
        "*How I created beautiful GitHub-style contribution graphs and traffic analytics for my dev.to articles using Kiro.dev - without writing much code myself*",
        "![Image description](/assets/img/5a9d039e9765.png)",
        "The Problem: Understanding Your Content's Impact",
        "As a technical writer publishing on dev.to, I found myself constantly wondering about my articles' performance. Sure, dev.to provides basic stats, but I wanted something more comprehensive - something that could show me:",
        "- **GitHub-style contribution grids** for my writing activity - **Traffic source analysis** to understand where readers discover my content - **Historical trends** to see how articles perform over time - **Beautiful visualizations** I could embed anywhere",
        "Working frequently with SEO teams, I was particularly interested in understanding traffic sources. Were my articles getting discovered through Google searches? LinkedIn shares? Twitter threads? Newsletter mentions? This data would be crucial for optimizing my content distribution strategy.",
        "The Solution: GitHub-Style Analytics with Traffic Insights",
        "What started as a simple idea turned into a comprehensive analytics system that fetches data from dev.to's API and generates beautiful SVG visualizations. The system creates GitHub-style contribution grids and tracks:",
        "📊 Core Metrics - **Views**: Daily article view counts - **Reactions**: Likes, unicorns, and bookmarks - **Comments**: Reader engagement levels - **Combined Activity**: Weighted scoring system",
        "🌐 Traffic Source Analysis The system tracks where your readers come from and visualizes it in a beautiful pie chart: - **Direct traffic** (users typing your URL directly) - **Search engines** (Google, Bing, DuckDuckGo) - **Social media** (LinkedIn, Twitter, Facebook) - **Developer platforms** (GitHub, dev.to internal traffic) - **Newsletters and blogs** (Substack, personal blogs) - **Professional tools** (Slack, Microsoft Office)",
        "![Traffic sources pie chart showing distribution of 19,096 total views](/assets/img/2bad2a13bc04.png)",
        "📈 GitHub-Style Contribution Grids The system generates six types of beautiful SVG visualizations:",
        "1. **Views Activity Grid** - GitHub-style contribution graph with green color scheme 2. **Reactions Activity Grid** - Purple-themed grid showing engagement patterns 3. **Combined Activity Grid** - Orange-themed weighted activity visualization 4. **Top Articles by Views** - Horizontal bar chart of most-viewed content 5. **Top Articles by Reactions** - Bar chart highlighting most engaging articles 6. **Traffic Sources Pie Chart** - Visual breakdown of where your readers come from",
        "The Magic: Built with Kiro.dev",
        "Here's where the story gets interesting - **I didn't write most of this code myself**. The entire project was created using [Kiro.dev](https://kiro.dev/), an AI-powered development environment that acts as your coding partner.",
        "How Kiro.dev Transformed My Workflow",
        "Instead of spending weeks researching APIs, designing data structures, and debugging visualization code, I simply described what I wanted:",
        "> \"I want to create GitHub-style contribution grids for my dev.to articles, track traffic sources, and generate beautiful SVG visualizations I can embed anywhere.\"",
        "Kiro.dev understood the requirements and generated the complete system:",
        "1. **Data fetching** - [`fetch_stats.py`](fetch_stats.py) handles API integration with error handling and rate limiting 2. **Contribution grids** - [`generate_advanced_graph.py`](generate_advanced_graph.py) creates GitHub-style activity visualizations 3. **Top articles charts** - [`generate_top_articles.py`](generate_top_articles.py) generates ranking visualizations 4. **Traffic analysis** - [`generate_traffic_pie_chart.py`](generate_traffic_pie_chart.py) creates traffic source breakdowns 5. **Automation** - [`.github/workflows/`](.github/workflows/) sets up daily updates 6. **Data management** - Smart incremental updates and referrer tracking",
        "Working with Kiro.dev felt like pair programming with an expert developer who: - **Understood context** - Grasped the full project scope from minimal descriptions - **Made smart decisions** - Chose appropriate data structures and algorithms - **Wrote clean code** - Generated well-documented, maintainable Python scripts - **Anticipated needs** - Added features I hadn't even thought of, like referrer tracking",
        "The AI didn't just write code - it architected a complete solution.",
        "The system organizes data efficiently:",
        "Each article file contains: - Basic metrics (views, comments, reactions) - Daily breakdown of activity - **Referrer data** showing traffic sources - Metadata for URL construction",
        "The system uses three dev.to API endpoints: - `/analytics/historical` - Historical analytics data - `/analytics/referrers` - Traffic source information - `/articles/me/published` - Article listings",
        "The fetcher implements intelligent incremental updates: - Only fetches new data since last update - Refreshes the second-to-last day to catch delayed analytics - Handles API rate limits gracefully - Maintains data integrity with backup strategies",
        "The SVG generation system creates publication-ready graphics: - **Responsive design** that works at any size - **Interactive tooltips** with detailed information - **Clickable elements** linking to original articles - **Multiple color schemes** for different contexts",
        "Key Insights from Traffic Analysis",
        "After implementing this system, I discovered fascinating patterns in my content's reach:",
        "Traffic Source Breakdown From my 19,096 total views, the top 10 referrers account for 14,974 views (78.4%):",
        "1. **Direct traffic (41.6%)** - 7,939 views from users directly accessing articles 2. **Google.com (24.3%)** - 4,639 views from organic search 3. **t.co (3.2%)** - 619 views from Twitter links 4. **LinkedIn.com (3.0%)** - 582 views from professional network shares 5. **dev.to (2.4%)** - 459 views from platform internal discovery 6. **office.net (0.9%)** - 172 views from Microsoft Office applications 7. **duckduckgo.com (0.9%)** - 172 views from privacy-focused search 8. **bing.com (0.8%)** - 158 views from Microsoft search 9. **facebook.com (0.6%)** - 121 views from social media shares 10. **mrugalski.pl (0.6%)** - 113 views from a personal blog referral",
        "This data proved invaluable for SEO strategy discussions: - **Search dominance** - Google alone drives 24.3% of traffic, with additional search engines contributing more - **Social amplification** - Twitter (3.2%) and LinkedIn (3.0%) show strong professional sharing - **Platform dynamics** - Only 2.4% comes from dev.to's internal discovery, showing external reach - **Direct engagement** - 41.6% direct traffic indicates strong brand recognition and bookmarking",
        "The top-performing article \"I bought us-east-1.com\" generated 9,021 views with fascinating traffic patterns: - **71% direct traffic** - Indicating strong word-of-mouth sharing - **6.6% Twitter traffic** - Viral social media spread - **5.7% LinkedIn traffic** - Professional network engagement",
        "Showcasing Results: Raw GitHub Content Integration",
        "One of the most powerful features is the ability to embed these visualizations anywhere using GitHub's raw content URLs. Here's how:",
        "Step 1: Generate Your Visualizations ```bash GitHub-style contribution grids python3 generate_advanced_graph.py --metric views --color github python3 generate_advanced_graph.py --metric reactions --color purple",
        "Top articles charts python3 generate_top_articles.py --metric reactions --count 3",
        "Traffic sources analysis python3 generate_traffic_pie_chart.py ```",
        "Step 2: Embed in README Files ```markdown ![Views Activity](/assets/img/1b55399b9f84.svg) ![Top Articles](/assets/img/81a2e8aa0cee.svg) ![Traffic Sources](/assets/img/e7a039bc0214.svg) ```",
        "Step 3: Use in Documentation The SVGs work perfectly in: - **GitHub README files** - Showcase your writing activity - **Portfolio websites** - Demonstrate content creation consistency - **Blog posts** - Visual proof of engagement metrics - **Social media** - Eye-catching performance summaries",
        "![README file showing embedded SVG graphs](/assets/img/4724e2869be9.png)",
        "1. **Repository structure** - Keep graphs in a dedicated `/graphs` folder 2. **Naming convention** - Use descriptive filenames like `devto_views_graph.svg` 3. **Update automation** - GitHub Actions ensures graphs stay current 4. **Multiple formats** - Generate different metrics and color schemes for various contexts",
        "GitHub Actions keeps everything current with daily updates:",
        "This means your analytics dashboard stays fresh without any manual intervention.",
        "1. AI-Assisted Development is Transformative Kiro.dev didn't just speed up development - it elevated the entire solution. The AI suggested features and optimizations I wouldn't have considered, like: - Logarithmic scaling for better visualization distribution - Referrer data aggregation for traffic source analysis - Incremental update strategies for API efficiency",
        "2. Data Visualization Drives Insights Having visual representations of my writing activity revealed patterns invisible in raw numbers: - Consistency gaps in publishing schedule - Correlation between article topics and engagement - Traffic source diversity indicating content reach",
        "3. SEO Integration is Crucial The referrer tracking proved invaluable for SEO discussions: - Identifying high-performing traffic sources - Understanding content discovery patterns - Optimizing distribution strategies",
        "Want to build your own analytics dashboard? The complete system is available as an open-source project. Here's how to get started:",
        "Quick Setup 1. Fork the repository - **[📊 devto-stats-github-action](https://github.com/gabrielkoo/devto-stats-github-action)** 2. Add your dev.to API key to GitHub Actions Secrets 3. Enable GitHub Actions for automatic updates 4. Customize visualizations to match your brand",
        "Customization Options - **Color schemes** - GitHub green, purple, blue, or orange themes - **Metrics focus** - Emphasize views, reactions, or combined activity - **Time ranges** - Adjust historical data collection periods - **Graph types** - Modify visualization styles and layouts",
        "Building this analytics dashboard taught me that the future of development isn't about replacing developers - it's about amplifying our capabilities. With Kiro.dev as my coding partner, I created a sophisticated system that would have taken weeks to build manually.",
        "The insights gained from traffic source analysis have already improved my content strategy, and the beautiful visualizations provide compelling evidence of writing consistency and engagement.",
        "Most importantly, this project demonstrates how AI-assisted development can help creators focus on what matters most - creating great content - while still building the tools needed to understand and optimize their impact.",
        "Whether you're a technical writer, developer advocate, or content creator, having detailed analytics about your work's reach and impact is invaluable. And with tools like Kiro.dev, building these systems is more accessible than ever.",
        "*Ready to build your own analytics dashboard? Check out the complete source code and start tracking your content's impact today.*",
        "**[📊 devto-stats-github-action](https://github.com/gabrielkoo/devto-stats-github-action)**",
        "![Sample](/assets/img/1151c14ece05.png)"
      ]
    },
    {
      "id": "article:why-i-built-a-web-ui-for-amazon-q-developer-cli-and-how-i-vibe-coded-it-54d6",
      "source_type": "article",
      "title": "Why I Built a Web UI for Amazon Q Developer CLI (And How I Vibe-Coded It)",
      "url": "https://gabrielkoo.com/blog/why-i-built-a-web-ui-for-amazon-q-developer-cli-and-how-i-vibe-coded-it-54d6/",
      "canonical_url": "https://dev.to/aws-builders/why-i-built-a-web-ui-for-amazon-q-developer-cli-and-how-i-vibe-coded-it-54d6",
      "published_at": "2025-06-26",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "ai",
        "vibecoding",
        "devex"
      ],
      "description": "🚀 Beyond the Console: Why I Built a Web UI for the Amazon Q Developer CLI Amazon Q is a...",
      "content": "## 🚀 **Beyond the Console: Why I Built a Web UI for the Amazon Q Developer CLI**\n\n[Amazon Q](https://aws.amazon.com/q/) is a **game-changer for developers**, but if you've only used it inside the **AWS Console**, you're only seeing part of the picture. Using Q in the Console is great for general AWS questions, but when it comes to **interacting with your own code and environment**, it hits fundamental limits.\n\nMy project started not from a corporate need, but from a personal one familiar to many developers: the **homelab**. I wanted a way to make quick fixes on my server from anywhere, but was constantly frustrated by the **clunky experience of mobile terminals**. This is the story of why I built the **Amazon Q Developer CLI WebUI**.\n\n## 📱 **The Real Problem: The Mobile Terminal Nightmare**\n\nPicture this: you're on the couch with your phone and have an idea for a quick change on your homelab server. You log in through a mobile web terminal and immediately hit a wall. Have you ever tried to **paste a multiline command** into one? Or **send a `Ctrl+C` to stop a runaway process**? It's a nightmare of awkward UIs, missed keystrokes, and immense frustration.\n\n### 🧱 **Example 1**: The Multi-Line `docker run` Command\n\nA common task in a homelab is running a new Docker container with specific ports and volumes:\n\n```bash\ndocker run -d --name=my-app \\\n  -p 8080:80 \\\n  -v $(pwd)/data:/app/data \\\n  nginx\n```\n\n**Mobile Frustrations**:\n\n* **Line Breaks**: Typing a backslash (`\\`) then hitting Enter is clumsy and error-prone on mobile keyboards.\n* **Special Characters**: Juggling `-`, `:`, `=`, and `$` requires constant switching between keyboard symbol layouts.\n* **Editing**: Fixing a typo in the middle of this command on a touchscreen is **incredibly difficult**.\n\n### 🔍 **Example 2**: The Piped `find` and `grep` Command\n\nYou need to search for a specific configuration line within all `.conf` files:\n\n```bash\nfind . -name \"*.conf\" | xargs grep \"listen_port\"\n```\n\n**Mobile Frustrations**:\n\n* **Keyboard Gymnastics**: This short command is packed with hard-to-access symbols on mobile keyboards: `.`, `*`, `\"`, and especially `|`.\n* **The Pipe (`|`)**: Often deeply hidden, requiring multiple taps to find and type.\n* **No `Ctrl` Key**: If a command hangs, the lack of an easy `Ctrl+C` makes stopping it a nightmare.\n\nThis frustration becomes a **major barrier** when using powerful tools like the **Amazon Q Developer CLI**. The CLI is the perfect assistant for the job—but it's **shackled to a terminal** that mobile browsers just can't handle.\n\n## 🤯 Other Reasons for Build This Project \n\n- You cannot install the great Amazon Q Developer CLI on **Windows** 🪟 unless you setup Windows Subsystem for Linux (WSL).\n- For other AI Agent alternatives, they usually belong to one of the cases below:\n  - Pure server side AI Agents - like Gemini/ChatGPT, they have access to tools/canvas and even run code for you - but they are not connected to your computer/servers\n  - CLI AI Agents - like Codex CLI, requires you to use in a Bring-Your-Own-Key way. You have to be very careful with your context length as you need to worry about token usage. \n\n## 🤖 **Q Developer CLI: An Agent with Better Tools**\n\nThink of the [Amazon Q Developer CLI](https://github.com/aws/amazon-q-developer-cli) as a **specialized AI agent** with more powerful \"tools\" than the AWS Console version. Its biggest advantage? **File system access**, giving it the **context it needs to be a true coding partner**.\n\nAlso look at how I vibe coded a Star Wars inspired lightsaber duel game: [Building A Plasma Sword Fighter Game with Amazon Q CLI](https://dev.to/aws-builders/building-a-plasma-sword-fighter-game-with-amazon-q-cli-279g)\n\n### 🔍 **Access Comparison**\n\n| Feature               | Q in AWS Console         | Amazon Q Developer CLI            | Amazon Q CLI via WebUI               |\n| --------------------- | ------------------------ | --------------------------------- | ------------------------------------ |\n| **Primary Use Case**  | General AWS service Q\\&A | Deep, contextual code development | Contextual development on any device |\n| **Code Awareness**    | ❌ No File System Access  | ✅ Full Project Context            | ✅ Full Project Context               |\n| **Interaction Model** | GUI Chat Window          | Command-Line Interface            | Browser-Based Terminal               |\n| **Accessibility**     | Requires AWS login       | Requires desktop terminal         | Access from any browser              |\n| **Mobile Usability**  | ✅ Serviceable UI         | ❌ Very Difficult                  | ✅ Designed for Web/Mobile            |\n\nThe table shows the gap: the **most powerful version** (CLI) is the **least accessible**—unless we bridge that gap.\n\n## 🛠️ **The Solution: A Self-Hosted Web Interface for the Q CLI**\n\n![Image description](/assets/img/b38418371754.jpg)\n\nSeeing it in action: <https://github.com/user-attachments/assets/99053791-17c5-4f09-bddb-d5b9ecd61cc0>\n\nMy vibe-coded solution: **Amazon Q Developer CLI WebUI**—a bridge to the full CLI that runs in **any modern browser**. It wraps around the native `q` command, exposing its full feature set without needing a traditional terminal.\n\nI started with telling Amazon Q Developer CLI _\"I want to re-create the Amazon Q Developer CLI experience in a web UI\"_. Q came up with the plan and the initial tech stack, I iterated a few rounds with testings - in less than an hour, my final solution came:\n\n**Tech Stack:**\n\n* Backend: `Node.js`, `Express`\n* Real-Time Communication: `socket.io`\n* Terminal Emulation: `node-pty` for spawning a real `pty`\n\nThe result? A **fluid, interactive CLI experience** from a browser—even on mobile.\n\n## 🚀 **Getting Started**\n\nMake sure you have [`Amazon Q Developer CLI` installed](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html).\n\n```bash\ngit clone --depth 1 https://github.com/gabrielkoo/amazon-q-developer-cli-webui\ncd amazon-q-developer-cli-webui\nnpm install\nq login && npm start  # —-host 0.0.0.0\n#\n# > amazon-q-cli-webui@0.0.1 start\n# > node server.js\n#\n# Server running on http://localhost:3000\n```\n\n## 🌐 **Making Web UI Available On Your Mobile**\n\nUse any of the typical homelab methods to expose `localhost:3000` online:\n\n* 🌍 **TailScale**\n* ☁️ **CloudFlare Tunnel**\n* 🚪 **Ngrok**\n* 🔐 **Open port 3000 + IP Whitelisting**\n\nThe first two options are preferred if you care very much about security.\n\n## 📲 **Unlocking a Truly Mobile Workflow**\n\nLet’s revisit the homelab scenario. You’re reviewing a Python script that needs a fix.\n\n* **Before**: You’d struggle with a mobile SSH app, fighting to type commands.\n* **After**: You just open a browser tab to your WebUI. Type: *\"Review `fix_script.py`, identify bugs, and suggest improvements.\"* Copy, paste, and interact—**touch-friendly and frustration-free**.\n\n![Image description](/assets/img/92f5982c1743.png)\n\n![Image description](/assets/img/6bba35601296.png)\n\n![Image description](/assets/img/f1eeb4214f14.png)\n\nLive demo: <https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2z9oxc4bm8waeor2nriy.gif>\n\n## ✨ **More Than a Wrapper: An Enhanced User Experience**\n\nI focused on **authenticity and comfort**, solving the mobile pain points:\n\n* 📝 **Proper Input**: A real multiline `<textarea>` makes command editing natural.\n* 💬 **Streaming Token Display**: Outputs appear with a \"typing\" effect, just like the CLI.\n* 🎨 **Full ANSI Support**: Colors, bolding, and formatting are preserved for readability.\n* 🤖 **Let Q Work for You**: Don’t sweat every keystroke—**Amazon Q can fix and run code for you**.\n\nThis project was born from a simple need: **use my favorite tools anywhere**. It’s about **unlocking the full power of Amazon Q**, even far from a real keyboard.\n\n🔗 **Check it out here**: [https://github.com/gabrielkoo/amazon-q-developer-cli-webui](https://github.com/gabrielkoo/amazon-q-developer-cli-webui)",
      "excerpts": [
        "🚀 **Beyond the Console: Why I Built a Web UI for the Amazon Q Developer CLI**",
        "[Amazon Q](https://aws.amazon.com/q/) is a **game-changer for developers**, but if you've only used it inside the **AWS Console**, you're only seeing part of the picture. Using Q in the Console is great for general AWS questions, but when it comes to **interacting with your own code and environment**, it hits fundamental limits.",
        "My project started not from a corporate need, but from a personal one familiar to many developers: the **homelab**. I wanted a way to make quick fixes on my server from anywhere, but was constantly frustrated by the **clunky experience of mobile terminals**. This is the story of why I built the **Amazon Q Developer CLI WebUI**.",
        "📱 **The Real Problem: The Mobile Terminal Nightmare**",
        "Picture this: you're on the couch with your phone and have an idea for a quick change on your homelab server. You log in through a mobile web terminal and immediately hit a wall. Have you ever tried to **paste a multiline command** into one? Or **send a `Ctrl+C` to stop a runaway process**? It's a nightmare of awkward UIs, missed keystrokes, and immense frustration.",
        "🧱 **Example 1**: The Multi-Line `docker run` Command",
        "A common task in a homelab is running a new Docker container with specific ports and volumes:",
        "* **Line Breaks**: Typing a backslash (`\\`) then hitting Enter is clumsy and error-prone on mobile keyboards. * **Special Characters**: Juggling `-`, `:`, `=`, and `$` requires constant switching between keyboard symbol layouts. * **Editing**: Fixing a typo in the middle of this command on a touchscreen is **incredibly difficult**.",
        "🔍 **Example 2**: The Piped `find` and `grep` Command",
        "You need to search for a specific configuration line within all `.conf` files:",
        "* **Keyboard Gymnastics**: This short command is packed with hard-to-access symbols on mobile keyboards: `.`, `*`, `\"`, and especially `|`. * **The Pipe (`|`)**: Often deeply hidden, requiring multiple taps to find and type. * **No `Ctrl` Key**: If a command hangs, the lack of an easy `Ctrl+C` makes stopping it a nightmare.",
        "This frustration becomes a **major barrier** when using powerful tools like the **Amazon Q Developer CLI**. The CLI is the perfect assistant for the job—but it's **shackled to a terminal** that mobile browsers just can't handle.",
        "🤯 Other Reasons for Build This Project",
        "- You cannot install the great Amazon Q Developer CLI on **Windows** 🪟 unless you setup Windows Subsystem for Linux (WSL). - For other AI Agent alternatives, they usually belong to one of the cases below: - Pure server side AI Agents - like Gemini/ChatGPT, they have access to tools/canvas and even run code for you - but they are not connected to your computer/servers - CLI AI Agents - like Codex CLI, requires you to use in a Bring-Your-Own-Key way. You have to be very careful with your context length as you need to worry about token usage.",
        "🤖 **Q Developer CLI: An Agent with Better Tools**",
        "Think of the [Amazon Q Developer CLI](https://github.com/aws/amazon-q-developer-cli) as a **specialized AI agent** with more powerful \"tools\" than the AWS Console version. Its biggest advantage? **File system access**, giving it the **context it needs to be a true coding partner**.",
        "Also look at how I vibe coded a Star Wars inspired lightsaber duel game: [Building A Plasma Sword Fighter Game with Amazon Q CLI](https://dev.to/aws-builders/building-a-plasma-sword-fighter-game-with-amazon-q-cli-279g)",
        "| Feature | Q in AWS Console | Amazon Q Developer CLI | Amazon Q CLI via WebUI | | --------------------- | ------------------------ | --------------------------------- | ------------------------------------ | | **Primary Use Case** | General AWS service Q\\&A | Deep, contextual code development | Contextual development on any device | | **Code Awareness** | ❌ No File System Access | ✅ Full Project Context | ✅ Full Project Context | | **Interaction Model** | GUI Chat Window | Command-Line Interface | Browser-Based Terminal | | **Accessibility** | Requires AWS login | Requires desktop terminal | Access from any browser | | **Mobile Usability** | ✅ Serviceable UI | ❌ Very Difficult | ✅ Designed for Web/Mobile |",
        "The table shows the gap: the **most powerful version** (CLI) is the **least accessible**—unless we bridge that gap.",
        "🛠️ **The Solution: A Self-Hosted Web Interface for the Q CLI**",
        "![Image description](/assets/img/b38418371754.jpg)",
        "My vibe-coded solution: **Amazon Q Developer CLI WebUI**—a bridge to the full CLI that runs in **any modern browser**. It wraps around the native `q` command, exposing its full feature set without needing a traditional terminal.",
        "I started with telling Amazon Q Developer CLI _\"I want to re-create the Amazon Q Developer CLI experience in a web UI\"_. Q came up with the plan and the initial tech stack, I iterated a few rounds with testings - in less than an hour, my final solution came:",
        "* Backend: `Node.js`, `Express` * Real-Time Communication: `socket.io` * Terminal Emulation: `node-pty` for spawning a real `pty`",
        "The result? A **fluid, interactive CLI experience** from a browser—even on mobile.",
        "Make sure you have [`Amazon Q Developer CLI` installed](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html).",
        "🌐 **Making Web UI Available On Your Mobile**",
        "Use any of the typical homelab methods to expose `localhost:3000` online:",
        "* 🌍 **TailScale** * ☁️ **CloudFlare Tunnel** * 🚪 **Ngrok** * 🔐 **Open port 3000 + IP Whitelisting**",
        "The first two options are preferred if you care very much about security.",
        "📲 **Unlocking a Truly Mobile Workflow**",
        "Let’s revisit the homelab scenario. You’re reviewing a Python script that needs a fix.",
        "* **Before**: You’d struggle with a mobile SSH app, fighting to type commands. * **After**: You just open a browser tab to your WebUI. Type: *\"Review `fix_script.py`, identify bugs, and suggest improvements.\"* Copy, paste, and interact—**touch-friendly and frustration-free**.",
        "![Image description](/assets/img/92f5982c1743.png)",
        "![Image description](/assets/img/6bba35601296.png)",
        "![Image description](/assets/img/f1eeb4214f14.png)",
        "✨ **More Than a Wrapper: An Enhanced User Experience**",
        "I focused on **authenticity and comfort**, solving the mobile pain points:",
        "* 📝 **Proper Input**: A real multiline ` ` makes command editing natural. * 💬 **Streaming Token Display**: Outputs appear with a \"typing\" effect, just like the CLI. * 🎨 **Full ANSI Support**: Colors, bolding, and formatting are preserved for readability. * 🤖 **Let Q Work for You**: Don’t sweat every keystroke—**Amazon Q can fix and run code for you**.",
        "This project was born from a simple need: **use my favorite tools anywhere**. It’s about **unlocking the full power of Amazon Q**, even far from a real keyboard.",
        "🔗 **Check it out here**: [https://github.com/gabrielkoo/amazon-q-developer-cli-webui](https://github.com/gabrielkoo/amazon-q-developer-cli-webui)"
      ]
    },
    {
      "id": "article:building-a-plasma-sword-fighter-game-with-amazon-q-cli-279g",
      "source_type": "article",
      "title": "Building A Plasma Sword Fighter Game with Amazon Q CLI",
      "url": "https://gabrielkoo.com/blog/building-a-plasma-sword-fighter-game-with-amazon-q-cli-279g/",
      "canonical_url": "https://dev.to/aws-builders/building-a-plasma-sword-fighter-game-with-amazon-q-cli-279g",
      "published_at": "2025-06-18",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "awschallenge",
        "genai"
      ],
      "description": "As a DevSecOps engineer, my daily grind usually involves CI/CD pipelines, security audits, and...",
      "content": "As a **DevSecOps engineer**, my daily grind usually involves **CI/CD pipelines**, **security audits**, and **infrastructure as code**. So, when the \"Build Games with Amazon Q CLI\" campaign popped up, it was a refreshing detour from the usual. The idea of conjuring a game with just **conversational prompts**, powered by **Amazon Q CLI's Claude 4 large language model**, was too intriguing to pass up. This isn't the usual realm of an \"enthusiast\" for me, but more of an exploration into how **AI can augment a developer's toolkit**, even outside their primary domain.\n\n## The Game: A \"Space Trilogy\" Inspired Plasma Sword Fighter ✨\n\nMy concept for the game was heavily inspired by the classic **\"Space Trilogy\" narratives** (you know the ones 😉), where the good guys wield **blue \"light swords\"** and the antagonists opt for menacing **red ones**. I wanted to capture that classic duel vibe, with players having **supernatural \"force push\" abilities** to add another layer to the combat. The result is a **\"Plasma Sword Fighter\" game** – a **two-player combat experience** featuring **real-time sword mechanics** and **tactical force pushes**. It's designed to be **intuitive, visually engaging**, and, importantly, **free from copyright entanglements** by using generic names.\n\n## Effective Prompting: Speaking the AI's Language 🗣️\n\nMy interaction with Amazon Q CLI was a rapid learning curve in **effective prompting**. Here’s what I found worked best:\n\n**Context is King**: I started by setting the scene: \"write a pygame on a streetfight style of star war light sword game, but don't use real names to avoid copyright.\" This broad stroke gave the AI its initial direction.\n\n![Initial Prompt](/assets/img/216c1c4fdc95.png)\n\n**Feature by Feature**: Instead of overwhelming the AI with a massive request, I let the AI to design an initial version first, followed by my later bug requests as well as feature enhancements, this allowed the AI to build the game incrementally.\n\n![Initial Features](/assets/img/ad701b2b31d9.png)\n\n**Leveraging Error Messages**: When things inevitably went sideways (as they do in development 🐛), I found Amazon Q CLI doing a good job on **auto-identifying the errors** from the command outputs in its initial runs, and it was able to **resolve the errors by itself** without my intervention.\n\n![Auto resolved package error](/assets/img/b08925e79cdd.png)\n\n**Refining Game Logic**: One of the more nuanced challenges was ensuring **continuous damage** when an opponent remained in the sword's active area. My prompt, `\"now there's a problem, the opponent's HP doesn't decrease for a 2nd time if the opponent stayed in the attack area of the plasma sword.` resulted in `You're right! The issue is that the combat detection only triggers once per attack due to the last_hit_time check. When a player holds down the attack button and the opponent stays in range, it should continue dealing damage. Let me fix this:\"`, guided Amazon Q CLI to implement a **time-based hit detection**, allowing for sustained damage while preventing hit-spamming with **invulnerability frames**.\n\n![Fixing the \"double hit\" issue](/assets/img/bc7e4b73582a.png)\n\n## AI as a Development Accelerator / Quick Prototype Generator ⚡\n\n**Amazon Q CLI**, recently powered by **Claude 4**, proved to be an **invaluable development partner**. It automated much of the heavy lifting, significantly reducing my **development time**:\n\n- **Boilerplate Generation**: The initial `pygame` setup, including window creation, basic event loops, and constant definitions, was generated almost instantly. This freed me from the mundane setup tasks.\n- **Core Game Mechanics**: From player movement and sword activation to force push mechanics and health management, the AI took my **high-level descriptions** and translated them into **functional code**.\n- **Smart Debugging**: The AI's ability to not only identify errors but also **suggest and implement fixes**, like installing missing libraries or correcting logical flaws in combat detection, was a **major time-saver**.\n- **Iterative Refinement**: The **back-and-forth process of prompting, testing, and refining** allowed for quick iterations and continuous improvement of the game's mechanics.\n\n## About the Code 💻\n\nThe Python code for the \"Plasma Sword Fighter\" game is straightforward and relies solely on the **`pygame` library**. While some of the combat and AI logic might appear \"raw\" to a seasoned game developer, offering room for more sophisticated refactoring (e.g., using state machines for AI), the current structure is **remarkably readable**. This clarity is a testament to the AI's ability to produce **understandable code**, even when generating complex interactions.\n\nThe full code is be hosted on GitHub here: <https://github.com/gabrielkoo/amazonq-plasma-sword-fighter-game/>\n\n## Screenshots and Gameplay 🎮\n\n![Image description](/assets/img/f2d63e851321.png)\n\n![Image description](/assets/img/ad28a54008f8.png)\n\n![Image description](/assets/img/e1e9045e84e7.png)\n\n![Image description](/assets/img/2d0d2cbd6f3e.png)\n\nHere are some snapshots from the \"Plasma Sword Fighter\" battles:\n\n- **Ready for Battle**: The game's initial screen, featuring two fighters against a cosmic backdrop, their health bars poised for action.\n- **Mid-Combat**: A dynamic shot showing the glowing plasma swords in action, with players engaged in a fierce duel.\n\n### Game Features:\n\n- **Two-player combat** with **glowing plasma swords** (avoiding copyright)\n- **Real-time combat system** with sword swinging and blocking\n- **Supernatural \"force push\" ability** with cooldown mechanics\n- **Health system** with visual health bars\n- **Invulnerability frames** after taking damage\n- **Visual effects** including sword glow and hit flashes\n- **Starfield background** for an immersive space combat feel\n- **AI opponent** with adjustable difficulty (Easy, Medium, Hard)\n\n### How to Play:\n\n- **Player 1 (Blue)**: WASD to move, SPACE to activate sword, SHIFT to attack, Q for force push, T to toggle targeting mode (mouse vs. auto-target).\n- **Player 2 (Red)**: Arrow keys to move, Right CTRL to activate sword, Right SHIFT to attack, ENTER for force push, P to toggle targeting mode (mouse vs. auto-target).\n- **AI Difficulty**: Press 1 for Easy, 2 for Medium, 3 for Hard.\n\n### Combat Mechanics:\n\n- Activate your plasma sword and maneuver close to your opponent.\n- Swing your sword to deal damage (**10 HP per hit**).\n- Utilize **force push** to knock back enemies and inflict minor damage (**5 HP + knockback**).\n- Each player starts with **100 HP**; the first to reach 0 loses.\n- Brief invulnerability periods after taking damage prevent spam attacks.\n\n### Game Controls:\n\n- Press **R to restart** after a game over.\n- Press **ESC to quit** anytime.\n\nThe \"Plasma Sword Fighter\" game captures the essence of classic space duels without infringing on any existing intellectual property. The visual effects create that iconic glowing sword aesthetic, offering a fun and engaging combat experience.\n\n## Final Thoughts 💡\n\nThis experience with Amazon Q CLI wasn't just about building a game; it was about understanding the practical applications of **GenAI in accelerating software development**. **Amazon Q CLI**, recently leveraging **Claude 4**, is a **powerful tool** that can significantly enhance productivity, even for those working outside traditional software development domains. It's a clear example of how **GenAI can democratize development**, allowing anyone with an idea to bring it to life with guided assistance. I'm genuinely impressed and encourage others to experiment with Amazon Q CLI to discover its potential firsthand.",
      "excerpts": [
        "As a **DevSecOps engineer**, my daily grind usually involves **CI/CD pipelines**, **security audits**, and **infrastructure as code**. So, when the \"Build Games with Amazon Q CLI\" campaign popped up, it was a refreshing detour from the usual. The idea of conjuring a game with just **conversational prompts**, powered by **Amazon Q CLI's Claude 4 large language model**, was too intriguing to pass up. This isn't the usual realm of an \"enthusiast\" for me, but more of an exploration into how **AI can augment a developer's toolkit**, even outside their primary domain.",
        "The Game: A \"Space Trilogy\" Inspired Plasma Sword Fighter ✨",
        "My concept for the game was heavily inspired by the classic **\"Space Trilogy\" narratives** (you know the ones 😉), where the good guys wield **blue \"light swords\"** and the antagonists opt for menacing **red ones**. I wanted to capture that classic duel vibe, with players having **supernatural \"force push\" abilities** to add another layer to the combat. The result is a **\"Plasma Sword Fighter\" game** – a **two-player combat experience** featuring **real-time sword mechanics** and **tactical force pushes**. It's designed to be **intuitive, visually engaging**, and, importantly, **free from copyright entanglements** by using generic names.",
        "Effective Prompting: Speaking the AI's Language 🗣️",
        "My interaction with Amazon Q CLI was a rapid learning curve in **effective prompting**. Here’s what I found worked best:",
        "**Context is King**: I started by setting the scene: \"write a pygame on a streetfight style of star war light sword game, but don't use real names to avoid copyright.\" This broad stroke gave the AI its initial direction.",
        "![Initial Prompt](/assets/img/216c1c4fdc95.png)",
        "**Feature by Feature**: Instead of overwhelming the AI with a massive request, I let the AI to design an initial version first, followed by my later bug requests as well as feature enhancements, this allowed the AI to build the game incrementally.",
        "![Initial Features](/assets/img/ad701b2b31d9.png)",
        "**Leveraging Error Messages**: When things inevitably went sideways (as they do in development 🐛), I found Amazon Q CLI doing a good job on **auto-identifying the errors** from the command outputs in its initial runs, and it was able to **resolve the errors by itself** without my intervention.",
        "![Auto resolved package error](/assets/img/b08925e79cdd.png)",
        "**Refining Game Logic**: One of the more nuanced challenges was ensuring **continuous damage** when an opponent remained in the sword's active area. My prompt, `\"now there's a problem, the opponent's HP doesn't decrease for a 2nd time if the opponent stayed in the attack area of the plasma sword.` resulted in `You're right! The issue is that the combat detection only triggers once per attack due to the last_hit_time check. When a player holds down the attack button and the opponent stays in range, it should continue dealing damage. Let me fix this:\"`, guided Amazon Q CLI to implement a **time-based hit detection**, allowing for sustained damage while preventing hit-spamming with **invulnerability frames**.",
        "![Fixing the \"double hit\" issue](/assets/img/bc7e4b73582a.png)",
        "AI as a Development Accelerator / Quick Prototype Generator ⚡",
        "**Amazon Q CLI**, recently powered by **Claude 4**, proved to be an **invaluable development partner**. It automated much of the heavy lifting, significantly reducing my **development time**:",
        "- **Boilerplate Generation**: The initial `pygame` setup, including window creation, basic event loops, and constant definitions, was generated almost instantly. This freed me from the mundane setup tasks. - **Core Game Mechanics**: From player movement and sword activation to force push mechanics and health management, the AI took my **high-level descriptions** and translated them into **functional code**. - **Smart Debugging**: The AI's ability to not only identify errors but also **suggest and implement fixes**, like installing missing libraries or correcting logical flaws in combat detection, was a **major time-saver**. - **Iterative Refinement**: The **back-and-forth process of prompting, testing, and refining** allowed for quick iterations and continuous improvement of the game's mechanics.",
        "The Python code for the \"Plasma Sword Fighter\" game is straightforward and relies solely on the **`pygame` library**. While some of the combat and AI logic might appear \"raw\" to a seasoned game developer, offering room for more sophisticated refactoring (e.g., using state machines for AI), the current structure is **remarkably readable**. This clarity is a testament to the AI's ability to produce **understandable code**, even when generating complex interactions.",
        "The full code is be hosted on GitHub here:",
        "![Image description](/assets/img/f2d63e851321.png)",
        "![Image description](/assets/img/ad28a54008f8.png)",
        "![Image description](/assets/img/e1e9045e84e7.png)",
        "![Image description](/assets/img/2d0d2cbd6f3e.png)",
        "Here are some snapshots from the \"Plasma Sword Fighter\" battles:",
        "- **Ready for Battle**: The game's initial screen, featuring two fighters against a cosmic backdrop, their health bars poised for action. - **Mid-Combat**: A dynamic shot showing the glowing plasma swords in action, with players engaged in a fierce duel.",
        "- **Two-player combat** with **glowing plasma swords** (avoiding copyright) - **Real-time combat system** with sword swinging and blocking - **Supernatural \"force push\" ability** with cooldown mechanics - **Health system** with visual health bars - **Invulnerability frames** after taking damage - **Visual effects** including sword glow and hit flashes - **Starfield background** for an immersive space combat feel - **AI opponent** with adjustable difficulty (Easy, Medium, Hard)",
        "- **Player 1 (Blue)**: WASD to move, SPACE to activate sword, SHIFT to attack, Q for force push, T to toggle targeting mode (mouse vs. auto-target). - **Player 2 (Red)**: Arrow keys to move, Right CTRL to activate sword, Right SHIFT to attack, ENTER for force push, P to toggle targeting mode (mouse vs. auto-target). - **AI Difficulty**: Press 1 for Easy, 2 for Medium, 3 for Hard.",
        "- Activate your plasma sword and maneuver close to your opponent. - Swing your sword to deal damage (**10 HP per hit**). - Utilize **force push** to knock back enemies and inflict minor damage (**5 HP + knockback**). - Each player starts with **100 HP**; the first to reach 0 loses. - Brief invulnerability periods after taking damage prevent spam attacks.",
        "- Press **R to restart** after a game over. - Press **ESC to quit** anytime.",
        "The \"Plasma Sword Fighter\" game captures the essence of classic space duels without infringing on any existing intellectual property. The visual effects create that iconic glowing sword aesthetic, offering a fun and engaging combat experience.",
        "This experience with Amazon Q CLI wasn't just about building a game; it was about understanding the practical applications of **GenAI in accelerating software development**. **Amazon Q CLI**, recently leveraging **Claude 4**, is a **powerful tool** that can significantly enhance productivity, even for those working outside traditional software development domains. It's a clear example of how **GenAI can democratize development**, allowing anyone with an idea to bring it to life with guided assistance. I'm genuinely impressed and encourage others to experiment with Amazon Q CLI to discover its potential firsthand."
      ]
    },
    {
      "id": "article:your-next-resume-could-be-more-than-a-pdf-say-notebooklm-57d6",
      "source_type": "article",
      "title": "Your Next Resume Could be More Than a PDF — Say NotebookLM",
      "url": "https://gabrielkoo.com/blog/your-next-resume-could-be-more-than-a-pdf-say-notebooklm-57d6/",
      "canonical_url": "https://dev.to/gabrielkoo/your-next-resume-could-be-more-than-a-pdf-say-notebooklm-57d6",
      "published_at": "2025-06-15",
      "last_verified_at": "2026-08-23",
      "tags": [
        "career",
        "ai",
        "notebooklm"
      ],
      "description": "For decades, the professional calling card has been a static document: the resume or the CV. We spend...",
      "content": "For decades, the professional calling card has been a static document: the resume or the CV. We spend hours meticulously crafting bullet points, trimming margins, and exporting to PDF, hoping to distill our complex careers into a single, digestible page.\n\nBut what if you could offer something more? What if you could let anyone: colleagues, recruiters, event organizers, or potential collaborators—literally *ask* your professional history questions using an interactive tool?\n\nWith the recent launch of public sharing for Google's NotebookLM, as [announced on the official Google blog](https://blog.google/technology/google-labs/notebooklm-public-notebooks/), this idea is no longer a \"what if.\" It's a reality. We can now create a personal, interactive notebook—complete with a chat interface, mind maps, and even audio summaries - that anyone can use to learn about us. It's poised to revolutionize how we represent ourselves professionally.\n\n## Beyond the Static Page\n\nThink about the limitations of a standard resume. It's a one way broadcast. It lists your skills but can't elaborate on them. It mentions your projects but can't explain the challenges you overcame. It's a summary, not a story.\n\nNow, imagine an alternative: a personal, public Google NotebookLM grounded in your complete digital footprint.\n\n![NotebookLM Chat Interface on My Profile](/assets/img/f170bb23dca8.png)\n\nFor example, as an AWS Community Builder in the DevSecOps and Cloud space, I grounded my own NotebookLM in my technical articles, community contributions, and detailed project experiences. This allows people to explore the depth of my expertise for any number of reasons, not just a job application.\n\n## What to Use as Your Source Material?\n\nThe power of your personal NotebookLM comes from the quality of the sources you provide. Think of it as creating a curated library about yourself. Here are some ideas to get you started:\n\n* **Your CV or Resume:** Start with the basics. Exporting your profile from LinkedIn as a PDF is a great foundation.\n* **Your Blog:** Include articles from platforms like dev.to, Medium, or your personal blog. This showcases your expertise and communication style.\n* **Your GitHub Profile:** You can save your profile page or specific repository READMEs as PDFs to include your code and project contributions.\n* **YouTube Videos:** For any talks, tutorials, or presentations you've given, you can use the video transcript as a source, allowing people to \"ask questions\" about your video content.\n* **Your Personal Site:** Your own website is a hub of information. Save key pages to add to your sources.\n\n## Why a Free, Managed Solution Wins\n\nFor the technically inclined, the idea of self-hosting a chatbot on a personal server, trained on your own documents, is tempting. However, this path comes with hidden complexities: recurring server costs, the technical burden of maintenance, and the constant worry about uptime. Your professional showcase shouldn't go down because of a server glitch.\n\nThis is where a free, managed solution like NotebookLM shines. It removes these barriers entirely:\n\n* **Zero Cost:** NotebookLM is free to use. You can create a rich, interactive experience without spending anything on hosting or infrastructure.\n* **No Maintenance Overhead:** Google handles the uptime, security, and scalability. You don’t need to be a server administrator to have a professional-grade interactive tool. You can focus on your story, not on system updates.\n* **Simplicity and Speed:** You can go from a collection of documents to a shareable, public notebook in minutes, not days. The interface is built for content creators, not just developers.\n\nBy handling the infrastructure, NotebookLM lets you focus on what truly matters: curating the best sources to represent your professional identity.\n\n## Introducing the \"Interactive You\"\n\nBy making a notebook public, you're essentially publishing a personalized research assistant about yourself. You're not just giving someone a document; you're inviting them into a conversation.\n\nThis opens up a new dimension of inquiry for anyone interested in your work. A hiring manager can still dig into the details, but now others can too.\n\n* A **conference organizer** might ask: \"Based on your articles, what are the three key takeaways you'd share in a talk about infrastructure as code?\"\n* A **community program manager** (like for AWS Community Builders) could inquire: \"How have you contributed to the tech community in the past year?\"\n* A **hiring manager** could still ask the specifics: \"What's your hands-on experience with scaling applications on AWS?\"\n\nThe AI, grounded exclusively in the sources *you* provided, would answer instantly, even citing the specific document where it found the information.\n\n![NotebookLM Data Sources](/assets/img/e55d0f88f481.png)\n\n## More Than Just Chat: A Mind Map of Your Career\n\nNotebookLM doesn't just enable chat; it helps you and your audience visualize connections. The Mind Map feature can automatically organize the concepts across all your source materials, creating a bird's-eye view of your skills, projects, and recurring themes of your work.\n\n![The Mindmap Generated by NotebookLM from Your Sources](/assets/img/ed01b365b4bd.png)\n\nImagine including this link in your speaker bio, your application for a community award, or alongside your traditional resume. You're not just *telling* them you're a good fit; you're giving them a tool to explore *why*.\n\n## The Future of Professional Identity is Interactive\n\nThe shift from a static resume to a conversational, public NotebookLM is more than just a novelty. It represents a fundamental change in how we share our professional stories. It's a move from a flat summary to a deep, explorable repository of your knowledge, ready to be queried for any purpose—from job applications to speaker proposals to community leadership roles.\n\nIt’s authentic, it's transparent, and it's incredibly powerful.\n\nWhy send a PDF when you can send an experience? It's time to stop telling people what you've done and start letting them ask. Go build your own interactive professional story and share it with the world.\n\n**Check out my public NotebookLM here:** <https://notebooklm.google.com/notebook/e22628a8-36a5-4482-b6ce-a7b720ec35de>\n\n## What If - I Live in a Place Where NotebookLM Access Is Limited\n\nI understand that NotebookLM is currently non available in some countries and regions (including the one I'm residing in). If you are one of those affected, do try one of my open sources solutions below:\n\nIf you already have an OpenVPN/WireGuard VPN:\n\n* https://github.com/gabrielkoo/wireguard-configs-for-ai-services\n* https://github.com/gabrielkoo/openvpn-configs-for-ai-services\n\nIf you have touched TailScale before:\n* https://github.com/gabrielkoo/tailscale-config-for-ai-services\n\nA common theme for these three projects is that instead of routing ALL TRAFFIC to the remote VPN (which severely slows down your internet browsing experience, and might causes e.g. Bank Apps to not work), my configurations would only route your IP geolocation restricted AI platforms' traffic to the VPN, while all remaining traffic still goes directly from your device. Under such a setup, you don't have to bother to always turning your existing VPN on and off -  saving your precious time.",
      "excerpts": [
        "For decades, the professional calling card has been a static document: the resume or the CV. We spend hours meticulously crafting bullet points, trimming margins, and exporting to PDF, hoping to distill our complex careers into a single, digestible page.",
        "But what if you could offer something more? What if you could let anyone: colleagues, recruiters, event organizers, or potential collaborators—literally *ask* your professional history questions using an interactive tool?",
        "With the recent launch of public sharing for Google's NotebookLM, as [announced on the official Google blog](https://blog.google/technology/google-labs/notebooklm-public-notebooks/), this idea is no longer a \"what if.\" It's a reality. We can now create a personal, interactive notebook—complete with a chat interface, mind maps, and even audio summaries - that anyone can use to learn about us. It's poised to revolutionize how we represent ourselves professionally.",
        "Think about the limitations of a standard resume. It's a one way broadcast. It lists your skills but can't elaborate on them. It mentions your projects but can't explain the challenges you overcame. It's a summary, not a story.",
        "Now, imagine an alternative: a personal, public Google NotebookLM grounded in your complete digital footprint.",
        "![NotebookLM Chat Interface on My Profile](/assets/img/f170bb23dca8.png)",
        "For example, as an AWS Community Builder in the DevSecOps and Cloud space, I grounded my own NotebookLM in my technical articles, community contributions, and detailed project experiences. This allows people to explore the depth of my expertise for any number of reasons, not just a job application.",
        "What to Use as Your Source Material?",
        "The power of your personal NotebookLM comes from the quality of the sources you provide. Think of it as creating a curated library about yourself. Here are some ideas to get you started:",
        "* **Your CV or Resume:** Start with the basics. Exporting your profile from LinkedIn as a PDF is a great foundation. * **Your Blog:** Include articles from platforms like dev.to, Medium, or your personal blog. This showcases your expertise and communication style. * **Your GitHub Profile:** You can save your profile page or specific repository READMEs as PDFs to include your code and project contributions. * **YouTube Videos:** For any talks, tutorials, or presentations you've given, you can use the video transcript as a source, allowing people to \"ask questions\" about your video content. * **Your Personal Site:** Your own website is a hub of information. Save key pages to add to your sources.",
        "Why a Free, Managed Solution Wins",
        "For the technically inclined, the idea of self-hosting a chatbot on a personal server, trained on your own documents, is tempting. However, this path comes with hidden complexities: recurring server costs, the technical burden of maintenance, and the constant worry about uptime. Your professional showcase shouldn't go down because of a server glitch.",
        "This is where a free, managed solution like NotebookLM shines. It removes these barriers entirely:",
        "* **Zero Cost:** NotebookLM is free to use. You can create a rich, interactive experience without spending anything on hosting or infrastructure. * **No Maintenance Overhead:** Google handles the uptime, security, and scalability. You don’t need to be a server administrator to have a professional-grade interactive tool. You can focus on your story, not on system updates. * **Simplicity and Speed:** You can go from a collection of documents to a shareable, public notebook in minutes, not days. The interface is built for content creators, not just developers.",
        "By handling the infrastructure, NotebookLM lets you focus on what truly matters: curating the best sources to represent your professional identity.",
        "Introducing the \"Interactive You\"",
        "By making a notebook public, you're essentially publishing a personalized research assistant about yourself. You're not just giving someone a document; you're inviting them into a conversation.",
        "This opens up a new dimension of inquiry for anyone interested in your work. A hiring manager can still dig into the details, but now others can too.",
        "* A **conference organizer** might ask: \"Based on your articles, what are the three key takeaways you'd share in a talk about infrastructure as code?\" * A **community program manager** (like for AWS Community Builders) could inquire: \"How have you contributed to the tech community in the past year?\" * A **hiring manager** could still ask the specifics: \"What's your hands-on experience with scaling applications on AWS?\"",
        "The AI, grounded exclusively in the sources *you* provided, would answer instantly, even citing the specific document where it found the information.",
        "![NotebookLM Data Sources](/assets/img/e55d0f88f481.png)",
        "More Than Just Chat: A Mind Map of Your Career",
        "NotebookLM doesn't just enable chat; it helps you and your audience visualize connections. The Mind Map feature can automatically organize the concepts across all your source materials, creating a bird's-eye view of your skills, projects, and recurring themes of your work.",
        "![The Mindmap Generated by NotebookLM from Your Sources](/assets/img/ed01b365b4bd.png)",
        "Imagine including this link in your speaker bio, your application for a community award, or alongside your traditional resume. You're not just *telling* them you're a good fit; you're giving them a tool to explore *why*.",
        "The Future of Professional Identity is Interactive",
        "The shift from a static resume to a conversational, public NotebookLM is more than just a novelty. It represents a fundamental change in how we share our professional stories. It's a move from a flat summary to a deep, explorable repository of your knowledge, ready to be queried for any purpose—from job applications to speaker proposals to community leadership roles.",
        "It’s authentic, it's transparent, and it's incredibly powerful.",
        "Why send a PDF when you can send an experience? It's time to stop telling people what you've done and start letting them ask. Go build your own interactive professional story and share it with the world.",
        "**Check out my public NotebookLM here:**",
        "What If - I Live in a Place Where NotebookLM Access Is Limited",
        "I understand that NotebookLM is currently non available in some countries and regions (including the one I'm residing in). If you are one of those affected, do try one of my open sources solutions below:",
        "If you already have an OpenVPN/WireGuard VPN:",
        "* https://github.com/gabrielkoo/wireguard-configs-for-ai-services * https://github.com/gabrielkoo/openvpn-configs-for-ai-services",
        "If you have touched TailScale before: * https://github.com/gabrielkoo/tailscale-config-for-ai-services",
        "A common theme for these three projects is that instead of routing ALL TRAFFIC to the remote VPN (which severely slows down your internet browsing experience, and might causes e.g. Bank Apps to not work), my configurations would only route your IP geolocation restricted AI platforms' traffic to the VPN, while all remaining traffic still goes directly from your device. Under such a setup, you don't have to bother to always turning your existing VPN on and off - saving your precious time."
      ]
    },
    {
      "id": "article:streamline-secure-self-service-developer-operations-with-aws-ssm-automation-runbooks-ne5",
      "source_type": "article",
      "title": "🚀 Streamline Secure, Self‑Service Developer Operations with AWS SSM Automation Runbooks 🎉",
      "url": "https://gabrielkoo.com/blog/streamline-secure-self-service-developer-operations-with-aws-ssm-automation-runbooks-ne5/",
      "canonical_url": "https://dev.to/aws-builders/streamline-secure-self-service-developer-operations-with-aws-ssm-automation-runbooks-ne5",
      "published_at": "2025-05-26",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "devops",
        "automation",
        "security"
      ],
      "description": "🎉 Dev Efficiency vs Deployment Freeze Ever wished you could just let your fellow software...",
      "content": "## 🎉 Dev Efficiency vs Deployment Freeze\n\nEver wished you could just let your fellow software engineers make chore changes to AWS workloads like Amazon ECS by themselves — without blowing up production or getting frozen out by business blackout windows? Buckle up, developers, because we’re about to introduce your new invisible DevOps sidekick: **AWS SSM Automation Runbook**! 🚀\n\n### The Great Dev Efficiency Freeze\n\nImagine running a critical application on Amazon ECS, where developers **must redeploy ECS container tasks** to:\n\n* Fix memory issues before a permanent application-layer solution is in place\n  (…because your engineering squad is off saving the world—or at least building the next killer feature.)\n* Update environment variables without causing downtime\n* ...etc\n\nNow add the twist: from **12:00 PM to 3:00 PM HKT**, our sales team conducts external demos. **Zero downtime** is non-negotiable. How can we keep dev efficiency soaring while uphold the deployment freeze requirement? ⏰\n\n---\n\n## Traditional Pitfalls (AKA The Villains)\n\n### 🚨 DIY Disaster: Direct ECS Access\n\nGranting developers direct ECS permissions feels fast, but:\n\n* **Steep learning curve**: AWS Console navigation or CLI setup can introduce errors\n* **High risk**: One misconfigured parameter can lead to downtime\n* **No enforcement**: Hard to embed custom rules like blocked demo hours\n\nImagine your developer making mistakes to one of the parameters below and executed the API to nuke your production site:\n![ecs API](/assets/img/15cd8694cef5.png)\n\n\n### 🛂 Cloud Engineer Bouncer: Gatekeeping Every Request\n\nCalling the cloud team for every deploy ensures safety, but:\n\n* **Bottleneck alert!** Scalability roadblock as your squad grows\n* **Productivity hit**: Cloud Engineers get pulled off strategic missions\n\n### 🛠️ Portal Overkill: Build-Your-Own Deployment Site\n\nA custom web portal can enforce biz logic, yet:\n\n* **High effort**: Development and maintenance drain cloud team resources\n* **Duplicate features**: Reinventing wheels AWS already made\n* And you will likely just tell yourself (or whoever proposing so as a \"solution\") — we are just a small team, we don't have resources for these FAANG level fancy developer friendly initiatives!\n\n---\n\n## Meet the Hero: AWS SSM Automation Runbook\n\nHere’s why AWS SSM Automation Runbook is the ultimate ally:\n\n* **Zero Custom UI** — Use the built-in, intuitive dropdown in the AWS Console\n* **One-Click Deep Links** — Skip account/role selection, teleport straight to your runbook\n* **Git-Powered Workflows** — YAML runbooks in Git for reviews, rollbacks, and CI/CD\n* **Approval Gates & Guardrails** — `aws:approve`, `aws:branch`, `aws:assertAwsResourceProperty` to enforce rules and time windows\n* **Centralized Audit Trail** — Push execution logs to CloudWatch for total traceability\n\n### Deep Link Magic\n\n```bash\nhttps://<identity-center-domain>/start/#/console \\\n  ?account_id=123456789012 \\\n  &role_name=DeployRole \\\n  &destination=https://us-east-1.console.aws.amazon.com/systems-manager/automation/execute/RefreshECSService\n# One link to rule them all! 🧙‍♂️\n```\n\nSeeing this in action: \n<https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yhwplhy7ztzx29ggsctn.gif>\n\nWith such a handy 1-click experience - your developers have no excuse in not adopting this new workflow!\n\n---\n\n### Automating Your ECS Deployment with Flair\n\n1. **Validate the Show’s Running Time**\n   * Check HKT; abort if within 12 PM – 3 PM.\n2. **Execute the Relevant AWS API**\n   * The automation step `aws:executeAwsApi` handles the ECS update under the hood—no manual CLI needed!\n3. **Handle Plot Twists**\n   * On failure: branch, alert via SNS or Slack, and let the team know someone drop‑kicked prod.\n\n![AWS SSM Automation Execution](/assets/img/49af4a14678b.png)\n\n---\n\n### Your New Best Friend in YAML\n\nHere's part of the runbook that I built for this scenario:\n\n```yaml\ndescription: \"Refresh ECS service with time-based guardrails\"\nassumeRole: \"arn:aws:iam::{{global:ACCOUNT_ID}}:role/DeployRole\"\n\nparameters:\n  EcsCluster:\n    type: String\n  EcsService:\n    type: String\n\nmainSteps:\n  - name: ValidateWindow\n    action: \"aws:branch\"\n    inputs:\n      Choices:\n        - NextStep: Abort\n  - name: DeployService\n    action: \"aws:executeAwsApi\"\n    inputs:\n      Service: ecs\n      Api: UpdateService\n      Cluster: \"{{EcsCluster}}\"\n      ServiceName: \"{{EcsService}}\"\n      ForceNewDeployment: true\n  - name: Notify\n    action: \"aws:executeWebhook\"\n    # Configure your SNS or Slack webhook here\n```\n\n> **Pro tip**: Use the UI editor for quick tweaks; commit your YAML for CI/CD muscle.\n\n![UI editor](/assets/img/d5baa3b2ea8c.png)\n\n### Extra Benefit: Extra Security\n\nGrant no direct `ecs:UpdateService` access for developers – Least privileged and avoid surprises:\n\n![Least privileged and avoid surprises](/assets/img/07ec937312ff.png)\n\nThe most critical API call in the example, `ecs:UpdateService`, is called with locked parameters - no risk of manual errors.\n![Locked parameters](/assets/img/c4d6e7c2a5b8.png)\n\n---\n\n## Extending to Your Own Workflows\n\nEnhance your toolbox with these quick-start recipes, ready to deploy across your AWS environment:\n\n### Safely Bust CDN Caches in CloudFront 🚀\n\nEnsure clients receive the latest assets without exceeding your invalidation limits. Create a runbook that:\n\n- Validates distribution status: Use `aws:assertAwsResourceProperty` to confirm there are no ongoing invalidations.\n- Invalidates a path pattern: Execute `aws:executeAwsApi` to call CloudFront’s `cloudfront:CreateInvalidation` API with specified paths.\n\n```yaml\nmainSteps:\n  - name: CheckDist\n    action: \"aws:assertAwsResourceProperty\"\n    inputs:\n      Service: cloudfront\n      Api: GetDistribution\n      Id: \"{{DistributionId}}\"\n      PropertySelector: \"Distribution.Status\"\n      DesiredValues: [\"Deployed\"]\n  - name: InvalidateCache\n    action: \"aws:executeAwsApi\"\n    inputs:\n      Service: cloudfront\n      Api: CreateInvalidation\n      DistributionId: \"{{DistributionId}}\"\n      InvalidationBatch:\n        CallerReference: \"{{Execution.Id}}\"\n        Paths:\n          Quantity: {{Paths.Count}}\n          Items: {{Paths.Items}}\n```\n\n### Schedule DB Snapshots with Graceful Throttling 🗄️\n\nAllow developers to request automated on-demand DB backups without overloading the production database:\n\n- Time-window guardrails: Restrict execution to off-peak hours using `aws:branch`.\n- Performance check: Use `aws:executeScript` to poll `rds:DescribeDBInstances` and ensure `CPUUtilization` is below the threshold.\n- Snapshot API call: Trigger `rds:CreateDBSnapshot` with `aws:executeAwsApi`.\n\n```yaml\nmainSteps:\n  - name: CheckIsPeakHours\n    action: \"aws:executeScript\"\n    inputs: {...}\n  - name: CheckWindow\n    action: \"aws:branch\"\n    inputs:\n      Choices:\n        - NextStep: TakeSnapshot\n          BooleanEquals: false\n          Variable: \"{{IsPeakHours}}\"\n  - name: WaitForLowCPU\n    action: \"aws:executeScript\"\n    inputs: {...}\n  - name: OptionalSleep\n    action: \"aws:sleep\"\n  - name: TakeSnapshot\n    action: \"aws:executeAwsApi\"\n    inputs:\n      Service: rds\n      Api: CreateDBSnapshot\n      DBInstanceIdentifier: \"{{DBInstance}}\"\n      DBSnapshotIdentifier: \"snapshot-{{Execution.Id}}\"\n```\n\n---\n\n## 🎯 Ready to Launch?\n\n1. Clone my [starter repo](https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security):\n   `git clone https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security`\n2. Adjust parameters and IAM roles for your account\n3. Author your own workflow with custom business logic checking\n4. Distribute the deep link to your team and enjoy secure, self-service common AWS operations!\n\nEmbrace secure, frictionless operations with AWS Systems Manager Automation today! 🚀\n\n## Extra - Why not AWS Step Functions?\n\nGreat question! AWS Step Functions are awesome for complex, long-running workflows, but here’s why AWS SSM Automation Runbooks might be a better fit for self-service developer operations:\n\n- **Simplicity & Speed**\n  SSM Automation has a focused, built-in UI for operational tasks. No need to define state machines or handle JSON-based state transitions—runbooks come with dropdowns and reduce setup time.\n\n- **Deep AWS Systems Manager Integration**\n   Automation actions like aws:approve, aws:branch, and aws:assertAwsResourceProperty are first-class citizens in SSM. Step Functions would require Lambda or other services to enforce the same guardrails.\n\n- **Permission Scoping**\n  Runbooks execute under a scoped IAM role you define. While Step Functions can assume roles too, SSM Runbooks make it explicit that every action is tied to your defined parameter inputs, and you don't need to worry about define a new AWS Lambda function for every custom script in case of Step Functions.\n\n- **Audit & Compliance**\n  Execution history is automatically recorded in Systems Manager. With Step Functions, you’d need CloudWatch Logs or X-Ray to tie everything together.\n\nThat said, if you need multi-account orchestration, fan-out/fan-in patterns, or integrate with external systems at scale, Step Functions can complement your automation. Choose SSM Runbooks for quick self-service ops, and Step Functions for complex, distributed workflows.",
      "excerpts": [
        "🎉 Dev Efficiency vs Deployment Freeze",
        "Ever wished you could just let your fellow software engineers make chore changes to AWS workloads like Amazon ECS by themselves — without blowing up production or getting frozen out by business blackout windows? Buckle up, developers, because we’re about to introduce your new invisible DevOps sidekick: **AWS SSM Automation Runbook**! 🚀",
        "The Great Dev Efficiency Freeze",
        "Imagine running a critical application on Amazon ECS, where developers **must redeploy ECS container tasks** to:",
        "* Fix memory issues before a permanent application-layer solution is in place (…because your engineering squad is off saving the world—or at least building the next killer feature.) * Update environment variables without causing downtime * ...etc",
        "Now add the twist: from **12:00 PM to 3:00 PM HKT**, our sales team conducts external demos. **Zero downtime** is non-negotiable. How can we keep dev efficiency soaring while uphold the deployment freeze requirement? ⏰",
        "Traditional Pitfalls (AKA The Villains)",
        "🚨 DIY Disaster: Direct ECS Access",
        "Granting developers direct ECS permissions feels fast, but:",
        "* **Steep learning curve**: AWS Console navigation or CLI setup can introduce errors * **High risk**: One misconfigured parameter can lead to downtime * **No enforcement**: Hard to embed custom rules like blocked demo hours",
        "Imagine your developer making mistakes to one of the parameters below and executed the API to nuke your production site: ![ecs API](/assets/img/15cd8694cef5.png)",
        "🛂 Cloud Engineer Bouncer: Gatekeeping Every Request",
        "Calling the cloud team for every deploy ensures safety, but:",
        "* **Bottleneck alert!** Scalability roadblock as your squad grows * **Productivity hit**: Cloud Engineers get pulled off strategic missions",
        "🛠️ Portal Overkill: Build-Your-Own Deployment Site",
        "A custom web portal can enforce biz logic, yet:",
        "* **High effort**: Development and maintenance drain cloud team resources * **Duplicate features**: Reinventing wheels AWS already made * And you will likely just tell yourself (or whoever proposing so as a \"solution\") — we are just a small team, we don't have resources for these FAANG level fancy developer friendly initiatives!",
        "Meet the Hero: AWS SSM Automation Runbook",
        "Here’s why AWS SSM Automation Runbook is the ultimate ally:",
        "* **Zero Custom UI** — Use the built-in, intuitive dropdown in the AWS Console * **One-Click Deep Links** — Skip account/role selection, teleport straight to your runbook * **Git-Powered Workflows** — YAML runbooks in Git for reviews, rollbacks, and CI/CD * **Approval Gates & Guardrails** — `aws:approve`, `aws:branch`, `aws:assertAwsResourceProperty` to enforce rules and time windows * **Centralized Audit Trail** — Push execution logs to CloudWatch for total traceability",
        "With such a handy 1-click experience - your developers have no excuse in not adopting this new workflow!",
        "Automating Your ECS Deployment with Flair",
        "1. **Validate the Show’s Running Time** * Check HKT; abort if within 12 PM – 3 PM. 2. **Execute the Relevant AWS API** * The automation step `aws:executeAwsApi` handles the ECS update under the hood—no manual CLI needed! 3. **Handle Plot Twists** * On failure: branch, alert via SNS or Slack, and let the team know someone drop‑kicked prod.",
        "![AWS SSM Automation Execution](/assets/img/49af4a14678b.png)",
        "Here's part of the runbook that I built for this scenario:",
        "parameters: EcsCluster: type: String EcsService: type: String",
        "mainSteps: - name: ValidateWindow action: \"aws:branch\" inputs: Choices: - NextStep: Abort - name: DeployService action: \"aws:executeAwsApi\" inputs: Service: ecs Api: UpdateService Cluster: \"{{EcsCluster}}\" ServiceName: \"{{EcsService}}\" ForceNewDeployment: true - name: Notify action: \"aws:executeWebhook\" # Configure your SNS or Slack webhook here ```",
        "> **Pro tip**: Use the UI editor for quick tweaks; commit your YAML for CI/CD muscle.",
        "![UI editor](/assets/img/d5baa3b2ea8c.png)",
        "Grant no direct `ecs:UpdateService` access for developers – Least privileged and avoid surprises:",
        "![Least privileged and avoid surprises](/assets/img/07ec937312ff.png)",
        "The most critical API call in the example, `ecs:UpdateService`, is called with locked parameters - no risk of manual errors. ![Locked parameters](/assets/img/c4d6e7c2a5b8.png)",
        "Extending to Your Own Workflows",
        "Enhance your toolbox with these quick-start recipes, ready to deploy across your AWS environment:",
        "Safely Bust CDN Caches in CloudFront 🚀",
        "Ensure clients receive the latest assets without exceeding your invalidation limits. Create a runbook that:",
        "- Validates distribution status: Use `aws:assertAwsResourceProperty` to confirm there are no ongoing invalidations. - Invalidates a path pattern: Execute `aws:executeAwsApi` to call CloudFront’s `cloudfront:CreateInvalidation` API with specified paths.",
        "Schedule DB Snapshots with Graceful Throttling 🗄️",
        "Allow developers to request automated on-demand DB backups without overloading the production database:",
        "- Time-window guardrails: Restrict execution to off-peak hours using `aws:branch`. - Performance check: Use `aws:executeScript` to poll `rds:DescribeDBInstances` and ensure `CPUUtilization` is below the threshold. - Snapshot API call: Trigger `rds:CreateDBSnapshot` with `aws:executeAwsApi`.",
        "1. Clone my [starter repo](https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security): `git clone https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security` 2. Adjust parameters and IAM roles for your account 3. Author your own workflow with custom business logic checking 4. Distribute the deep link to your team and enjoy secure, self-service common AWS operations!",
        "Embrace secure, frictionless operations with AWS Systems Manager Automation today! 🚀",
        "Extra - Why not AWS Step Functions?",
        "Great question! AWS Step Functions are awesome for complex, long-running workflows, but here’s why AWS SSM Automation Runbooks might be a better fit for self-service developer operations:",
        "- **Simplicity & Speed** SSM Automation has a focused, built-in UI for operational tasks. No need to define state machines or handle JSON-based state transitions—runbooks come with dropdowns and reduce setup time.",
        "- **Deep AWS Systems Manager Integration** Automation actions like aws:approve, aws:branch, and aws:assertAwsResourceProperty are first-class citizens in SSM. Step Functions would require Lambda or other services to enforce the same guardrails.",
        "- **Permission Scoping** Runbooks execute under a scoped IAM role you define. While Step Functions can assume roles too, SSM Runbooks make it explicit that every action is tied to your defined parameter inputs, and you don't need to worry about define a new AWS Lambda function for every custom script in case of Step Functions.",
        "- **Audit & Compliance** Execution history is automatically recorded in Systems Manager. With Step Functions, you’d need CloudWatch Logs or X-Ray to tie everything together.",
        "That said, if you need multi-account orchestration, fan-out/fan-in patterns, or integrate with external systems at scale, Step Functions can complement your automation. Choose SSM Runbooks for quick self-service ops, and Step Functions for complex, distributed workflows."
      ]
    },
    {
      "id": "article:phantom-dns-query-to-gcp-vm-metadata-service-in-my-aws-workload-revealed-by-route-53-resolver-3c75",
      "source_type": "article",
      "title": "Phantom DNS Query to GCP VM Metadata Service in My AWS Workload 👻 — Revealed by Route 53 Resolver Logging 🔍",
      "url": "https://gabrielkoo.com/blog/phantom-dns-query-to-gcp-vm-metadata-service-in-my-aws-workload-revealed-by-route-53-resolver-3c75/",
      "canonical_url": "https://dev.to/aws-builders/phantom-dns-query-to-gcp-vm-metadata-service-in-my-aws-workload-revealed-by-route-53-resolver-3c75",
      "published_at": "2025-04-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "security",
        "dns",
        "firewall"
      ],
      "description": "I Found Suspicious DNS Calls on AWS I was debugging flaky outbound calls from an AWS...",
      "content": "## I Found Suspicious DNS Calls on AWS\n\nI was debugging flaky outbound calls from an **AWS Fargate task**. To gain better visibility into potential network issues, I followed the common best practice of enabling **Route 53 Resolver query logging** for DNS queries within my VPC on AWS. Shortly after the logs started populating, while examining them for clues related to the original outbound problems, I noticed something odd...\n\nI noticed repeated lookups for **`metadata.google.internal.`** — an endpoint meant **exclusively for Google Cloud VMs**, but hey, my workload was on AWS all the time!\n\nAt first glance, it felt like a **hacker reconnaissance** probing my AWS Fargate tasks 🕵️‍♂️. In reality, it was an open-source Application Performance Monitoring (APM) agent auto-detecting the cloud provider by testing GCP metadata when AWS metadata was unavailable (AWS Fargate tasks don't expose the EC2 instance metadata endpoint at `169.254.169.254`). Without logging, these phantom calls would’ve remained invisible 👻.\n\n---\n\n## Why Enable Resolver Query Logging?\n\n1. 🔍 **Complete DNS Visibility**  \n   Capture every query name, source IP/instance ID, response code, and timestamp — no more blind spots, quick identification of the culprit workload.\n\n2. 🛡️ **Security Insights & Threat Hunting**  \n   Spot malware or phishing domain lookups. Feed logs into SIEMs for automated alerting.\n\n3. 📋 **Audit & Compliance**  \n   Demonstrate continuous network monitoring for regulated workloads (PCI, HIPAA, etc.).\n\n4. 🚑 **Faster Troubleshooting**  \n   Diagnose application failures by correlating DNS resolution errors with service issues.\n\n## Quick Start: Enable Query Logging & Basic DNS Firewall via CloudFormation\n\nThis template enables comprehensive query logging and sets up a basic DNS Firewall rule group to get you started - get query logging live in minutes. \n\nSimply paste it into your stack, provide your VPC ID, and start capturing **all** DNS queries to CloudWatch Logs 🛠️📝:\n\n```yaml\nAWSTemplateFormatVersion: '2010-09-09'\nDescription: >\n  Route 53 Resolver DNS Firewall with VPC‑level DNS **query logging**.\n  Logs **all** DNS queries for a supplied VPC to CloudWatch Logs.\n\nParameters:\n  VpcId:\n    Type: AWS::EC2::VPC::Id\n    Description: VPC ID to protect and monitor.\n  AssociationPriority:\n    Type: Number\n    Default: 150\n    Description: 'Priority (100‑9900) for the VPC firewall association; must be unique within the VPC.'\n    MinValue: 100\n    MaxValue: 9900\n  LogRetentionDays:\n    Type: Number\n    Default: 30\n    Description: Retention (days) for the CloudWatch Logs group.\n\nResources:\n\n  QueryLogGroup:\n    Type: AWS::Logs::LogGroup\n    Properties:\n      RetentionInDays: !Ref LogRetentionDays\n\n  BlockedDomains:\n    Type: AWS::Route53Resolver::FirewallDomainList\n    Properties:\n      Name: blocked-domains\n      Domains:\n        - badexample.com\n        - malware.example\n        - phishing.test\n\n  FirewallRuleGroup:\n    Type: AWS::Route53Resolver::FirewallRuleGroup\n    Properties:\n      Name: vpc-dns-firewall-rule-group\n      FirewallRules:\n        - FirewallDomainListId: !Ref BlockedDomains\n          Priority: 10\n          Action: BLOCK\n          BlockResponse: NODATA\n\n  FirewallAssociation:\n    Type: AWS::Route53Resolver::FirewallRuleGroupAssociation\n    Properties:\n      FirewallRuleGroupId: !Ref FirewallRuleGroup\n      VpcId: !Ref VpcId\n      Priority: !Ref AssociationPriority\n      Name: vpc-dns-firewall-association\n\n  DNSQueryLogConfig:\n    Type: AWS::Route53Resolver::ResolverQueryLoggingConfig\n    Properties:\n      Name: vpc-dns-query-logs\n      DestinationArn: !GetAtt QueryLogGroup.Arn\n\n  DNSQueryLogAssociation:\n    Type: AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation\n    Properties:\n      ResolverQueryLogConfigId: !Ref DNSQueryLogConfig\n      ResourceId: !Ref VpcId\n\n  DNSQueryLogsPolicy:\n    Type: AWS::Logs::ResourcePolicy\n    Properties:\n      PolicyName: route53resolver-query-logs\n      PolicyDocument: !Sub |\n        {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Sid\": \"Route53ResolverQueryLogsToCloudWatch\",\n              \"Effect\": \"Allow\",\n              \"Principal\": { \"Service\": \"route53resolver.amazonaws.com\" },\n              \"Action\": [\n                \"logs:CreateLogStream\",\n                \"logs:PutLogEvents\"\n              ],\n              \"Resource\": \"arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${QueryLogGroup}:*\"\n            }\n          ]\n        }\n\nOutputs:\n  FirewallRuleGroupId:\n    Description: ID of the DNS Firewall Rule Group\n    Value: !Ref FirewallRuleGroup\n  QueryLogConfigId:\n    Description: ID of the Resolver Query Log Config\n    Value: !Ref DNSQueryLogConfig\n  QueryLogGroupName:\n    Description: CloudWatch Logs group for DNS query logs\n    Value: !Ref QueryLogGroup\n```  \n\nAlternatively, download it from my GitHub repository: \n<https://github.com/gabrielkoo/aws-route53-dns-firewall-logging-cfn>\n\nOnce the template has been deployed, go to CloudWatch Logs and start inspecting the DNS queries made in your VPC! \n\n### 📄 Sample Log Entry from a Blocked DNS Query\n\nWhen you add DNS Firewall, blocked lookups surface with an action field. Here’s how a blocked domain appears in CloudWatch Logs / S3 log file:\n\n```json\n{\n    \"version\": \"1.100000\",\n    \"account_id\": \"452954105288\",\n    \"region\": \"us-east-1\",\n    \"vpc_id\": \"vpc-0b20bb8a5eb28cb99\",\n    \"query_timestamp\": \"2025-04-27T04:45:23Z\",\n    \"query_name\": \"badexample.com.\",\n    \"query_type\": \"A\",\n    \"query_class\": \"IN\",\n    \"rcode\": \"NOERROR\",\n    \"answers\": [],\n    \"srcaddr\": \"10.0.101.123\",\n    \"srcport\": \"46275\",\n    \"transport\": \"UDP\",\n    \"srcids\": {\n        \"instance\": \"i-0cf6fbb4f8960f245\"\n    },\n    \"firewall_rule_action\": \"BLOCK\",\n    \"firewall_rule_group_id\": \"rslvr-frg-69632fc987684fbd\",\n    \"firewall_domain_list_id\": \"rslvr-fdl-de20dd498c14dce\"\n}\n```  \n\n### Layer on Route 53 DNS Firewall\n\nThe provided CloudFormation template establishes a basic DNS Firewall. You can further enhance this by:\n\n1. 🧠 **Managed Threat Lists** – Block known malicious domains without manual upkeep.  \n2. ✅ **Custom Allow/Deny Rules** – Enforce corporate-approved domains.  \n3. ⚡ **Real-Time Enforcement** – Decide between BLOCK, ALERT, or TRUNCATE responses.\n\nApply firewall policies at the VPC level — no per-instance agents needed.\n\n---\n\n## What actually happened with the `metadata.google.internal` DNS query\n\nAgain, my workload was a Python Flask app with Elastic APM installed, running on AWS Fargate.\n\n[elastic/apm-agent-python:elasticapm/base.py#L419-L456](\nhttps://github.com/elastic/apm-agent-python/blob/099fefe411cd449af9a1ea35529fb66c51de0b7b/elasticapm/base.py#L419-L456)\n```python\ndef get_cloud_info(self):\n    provider = str(self.config.cloud_provider).lower()\n\n    if not provider or provider == \"none\" or provider == \"false\":\n        return {}\n    if provider == \"aws\":\n        # This line was not hit as I didn't explicitly configure the `cloud_provider` option in my Elastic APM setup. \n        ...\n    elif provider == \"gcp\":\n        ...\n    elif provider == \"azure\":\n        ...\n    elif provider == \"auto\" or provider == \"true\":\n        data = {}\n        # This line returned `None` as my workload was based on AWS Fargate, which does not support the EC2 Metadata endpoint.\n        data = cloud.aws_metadata()\n        if data:\n            return data\n        # This line caused the DNS query.\n        data = cloud.gcp_metadata()\n        if data:\n            return data\n        data = cloud.azure_metadata()\n        return data\n    else:\n        self.logger.warning(\"Unknown value for CLOUD_PROVIDER, skipping cloud metadata: {}\".format(provider))\n        return {}\n```\n\nAs I didn't configure the cloud provider, the script was being handy to auto detect the VM's cloud provider by trial and error, starting with **AWS EC2's metadata endpoint** that didn't work as my workload was running on AWS Fargate. It then attempted to detect if the workload was running on Google Cloud.\n\nThe AWS and Azure metadata endpoints were both IP based, so no relevant DNS queries were detected by the Route 53 Resolver query logging.\n\n## Another Real-World Case: Ubuntu `cloud-init` Tests\n\nAnother observation that I made after enabling the DNS query logging was that, on standalone cloud Ubuntu servers, `cloud-init` queries `does-not-exist.example.com.` to detect DNS interception. These benign checks contaminate DNS logs unless you filter them out—another great reason to keep query logging on.\n\n[canonical/cloud:cloudinit/util.py#L1297-L1318](\nhttps://github.com/canonical/cloud-init/blob/589c9461db16570789ab412e44be53a19f85b8ee/cloudinit/util.py#L1297-L1318):\n```python\nif _DNS_REDIRECT_IP is None:\n    badips = set()\n    badnames = (\n        \"does-not-exist.example.com.\",\n        \"example.invalid.\",\n        \"__cloud_init_expected_not_found__\",\n    )\n    badresults: dict = {}\n    for iname in badnames:\n        try:\n            result = socket.getaddrinfo(\n                iname, None, 0, 0, socket.SOCK_STREAM, socket.AI_CANONNAME\n            )\n            badresults[iname] = []\n            for _fam, _stype, _proto, cname, sockaddr in result:\n                badresults[iname].append(\"%s: %s\" % (cname, sockaddr[0]))\n                badips.add(sockaddr[0])\n        except (socket.gaierror, socket.error):\n            pass\n    _DNS_REDIRECT_IP = badips\n    if badresults:\n        LOG.debug(\"detected dns redirection: %s\", badresults)\n```\n\nAnother unexpected finding — but thankfully, it wasn’t an actual exploit 😅.\n\n---\n\n## Best Practices\n\nSo now we have gone through on how to setup a Route 53 Resolver with query logging and a simple firewall. Here a few more points for you to take further steps:\n\n- 🧱 **Automate with IaC**: Use CloudFormation/Terraform for consistent setup across environments.  \n- 📊 **Centralize Logs**: Stream to CloudWatch Logs or S3, and integrate with security platforms, or alternatively perform automated analysis on aggregated data, potentially using Generative AI.\n- 🧾 **Tune Policies**: Review and whitelist legitimate domains to avoid false positives - make the domain lists version controlled with say git.\n- 🔁 **Periodic Review**: Analyze logs regularly to refine firewall rules and detect new patterns.\n\n---\n\n## Related\n\nGetting more serious on DNS security after this article? Also read another article of mine: \n\n<https://dev.to/aws-builders/i-bought-us-east-1com-a-look-at-security-dns-traffic-and-protecting-aws-users-15ng>\n\n## Conclusion\n\nTurning on Route 53 Resolver query logging can transform DNS into a powerful diagnostic and security tool 🧠🔐. Paired with DNS Firewall, you gain both visibility and control—preventing unwanted traffic, uncovering phantom queries, and strengthening your AWS network posture.\n\nStay curious, stay secure! 🛡️✨",
      "excerpts": [
        "I Found Suspicious DNS Calls on AWS",
        "I was debugging flaky outbound calls from an **AWS Fargate task**. To gain better visibility into potential network issues, I followed the common best practice of enabling **Route 53 Resolver query logging** for DNS queries within my VPC on AWS. Shortly after the logs started populating, while examining them for clues related to the original outbound problems, I noticed something odd...",
        "I noticed repeated lookups for **`metadata.google.internal.`** — an endpoint meant **exclusively for Google Cloud VMs**, but hey, my workload was on AWS all the time!",
        "At first glance, it felt like a **hacker reconnaissance** probing my AWS Fargate tasks 🕵️‍♂️. In reality, it was an open-source Application Performance Monitoring (APM) agent auto-detecting the cloud provider by testing GCP metadata when AWS metadata was unavailable (AWS Fargate tasks don't expose the EC2 instance metadata endpoint at `169.254.169.254`). Without logging, these phantom calls would’ve remained invisible 👻.",
        "Why Enable Resolver Query Logging?",
        "1. 🔍 **Complete DNS Visibility** Capture every query name, source IP/instance ID, response code, and timestamp — no more blind spots, quick identification of the culprit workload.",
        "2. 🛡️ **Security Insights & Threat Hunting** Spot malware or phishing domain lookups. Feed logs into SIEMs for automated alerting.",
        "3. 📋 **Audit & Compliance** Demonstrate continuous network monitoring for regulated workloads (PCI, HIPAA, etc.).",
        "4. 🚑 **Faster Troubleshooting** Diagnose application failures by correlating DNS resolution errors with service issues.",
        "Quick Start: Enable Query Logging & Basic DNS Firewall via CloudFormation",
        "This template enables comprehensive query logging and sets up a basic DNS Firewall rule group to get you started - get query logging live in minutes.",
        "Simply paste it into your stack, provide your VPC ID, and start capturing **all** DNS queries to CloudWatch Logs 🛠️📝:",
        "Parameters: VpcId: Type: AWS::EC2::VPC::Id Description: VPC ID to protect and monitor. AssociationPriority: Type: Number Default: 150 Description: 'Priority (100‑9900) for the VPC firewall association; must be unique within the VPC.' MinValue: 100 MaxValue: 9900 LogRetentionDays: Type: Number Default: 30 Description: Retention (days) for the CloudWatch Logs group.",
        "QueryLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: !Ref LogRetentionDays",
        "BlockedDomains: Type: AWS::Route53Resolver::FirewallDomainList Properties: Name: blocked-domains Domains: - badexample.com - malware.example - phishing.test",
        "FirewallRuleGroup: Type: AWS::Route53Resolver::FirewallRuleGroup Properties: Name: vpc-dns-firewall-rule-group FirewallRules: - FirewallDomainListId: !Ref BlockedDomains Priority: 10 Action: BLOCK BlockResponse: NODATA",
        "FirewallAssociation: Type: AWS::Route53Resolver::FirewallRuleGroupAssociation Properties: FirewallRuleGroupId: !Ref FirewallRuleGroup VpcId: !Ref VpcId Priority: !Ref AssociationPriority Name: vpc-dns-firewall-association",
        "DNSQueryLogConfig: Type: AWS::Route53Resolver::ResolverQueryLoggingConfig Properties: Name: vpc-dns-query-logs DestinationArn: !GetAtt QueryLogGroup.Arn",
        "DNSQueryLogAssociation: Type: AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation Properties: ResolverQueryLogConfigId: !Ref DNSQueryLogConfig ResourceId: !Ref VpcId",
        "DNSQueryLogsPolicy: Type: AWS::Logs::ResourcePolicy Properties: PolicyName: route53resolver-query-logs PolicyDocument: !Sub | { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Sid\": \"Route53ResolverQueryLogsToCloudWatch\", \"Effect\": \"Allow\", \"Principal\": { \"Service\": \"route53resolver.amazonaws.com\" }, \"Action\": [ \"logs:CreateLogStream\", \"logs:PutLogEvents\" ], \"Resource\": \"arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${QueryLogGroup}:*\" } ] }",
        "Outputs: FirewallRuleGroupId: Description: ID of the DNS Firewall Rule Group Value: !Ref FirewallRuleGroup QueryLogConfigId: Description: ID of the Resolver Query Log Config Value: !Ref DNSQueryLogConfig QueryLogGroupName: Description: CloudWatch Logs group for DNS query logs Value: !Ref QueryLogGroup ```",
        "Alternatively, download it from my GitHub repository:",
        "Once the template has been deployed, go to CloudWatch Logs and start inspecting the DNS queries made in your VPC!",
        "📄 Sample Log Entry from a Blocked DNS Query",
        "When you add DNS Firewall, blocked lookups surface with an action field. Here’s how a blocked domain appears in CloudWatch Logs / S3 log file:",
        "Layer on Route 53 DNS Firewall",
        "The provided CloudFormation template establishes a basic DNS Firewall. You can further enhance this by:",
        "1. 🧠 **Managed Threat Lists** – Block known malicious domains without manual upkeep. 2. ✅ **Custom Allow/Deny Rules** – Enforce corporate-approved domains. 3. ⚡ **Real-Time Enforcement** – Decide between BLOCK, ALERT, or TRUNCATE responses.",
        "Apply firewall policies at the VPC level — no per-instance agents needed.",
        "What actually happened with the `metadata.google.internal` DNS query",
        "Again, my workload was a Python Flask app with Elastic APM installed, running on AWS Fargate.",
        "[elastic/apm-agent-python:elasticapm/base.py#L419-L456]( https://github.com/elastic/apm-agent-python/blob/099fefe411cd449af9a1ea35529fb66c51de0b7b/elasticapm/base.py#L419-L456) ```python def get_cloud_info(self): provider = str(self.config.cloud_provider).lower()",
        "if not provider or provider == \"none\" or provider == \"false\": return {} if provider == \"aws\": # This line was not hit as I didn't explicitly configure the `cloud_provider` option in my Elastic APM setup. ... elif provider == \"gcp\": ... elif provider == \"azure\": ... elif provider == \"auto\" or provider == \"true\": data = {} # This line returned `None` as my workload was based on AWS Fargate, which does not support the EC2 Metadata endpoint. data = cloud.aws_metadata() if data: return data # This line caused the DNS query. data = cloud.gcp_metadata() if data: return data data = cloud.azure_metadata() return data else: self.logger.warning(\"Unknown value for CLOUD_PROVIDER, skipping cloud metadata: {}\".format(provider)) return {} ```",
        "As I didn't configure the cloud provider, the script was being handy to auto detect the VM's cloud provider by trial and error, starting with **AWS EC2's metadata endpoint** that didn't work as my workload was running on AWS Fargate. It then attempted to detect if the workload was running on Google Cloud.",
        "The AWS and Azure metadata endpoints were both IP based, so no relevant DNS queries were detected by the Route 53 Resolver query logging.",
        "Another Real-World Case: Ubuntu `cloud-init` Tests",
        "Another observation that I made after enabling the DNS query logging was that, on standalone cloud Ubuntu servers, `cloud-init` queries `does-not-exist.example.com.` to detect DNS interception. These benign checks contaminate DNS logs unless you filter them out—another great reason to keep query logging on.",
        "[canonical/cloud:cloudinit/util.py#L1297-L1318]( https://github.com/canonical/cloud-init/blob/589c9461db16570789ab412e44be53a19f85b8ee/cloudinit/util.py#L1297-L1318): ```python if _DNS_REDIRECT_IP is None: badips = set() badnames = ( \"does-not-exist.example.com.\", \"example.invalid.\", \"__cloud_init_expected_not_found__\", ) badresults: dict = {} for iname in badnames: try: result = socket.getaddrinfo( iname, None, 0, 0, socket.SOCK_STREAM, socket.AI_CANONNAME ) badresults[iname] = [] for _fam, _stype, _proto, cname, sockaddr in result: badresults[iname].append(\"%s: %s\" % (cname, sockaddr[0])) badips.add(sockaddr[0]) except (socket.gaierror, socket.error): pass _DNS_REDIRECT_IP = badips if badresults: LOG.debug(\"detected dns redirection: %s\", badresults) ```",
        "Another unexpected finding — but thankfully, it wasn’t an actual exploit 😅.",
        "So now we have gone through on how to setup a Route 53 Resolver with query logging and a simple firewall. Here a few more points for you to take further steps:",
        "- 🧱 **Automate with IaC**: Use CloudFormation/Terraform for consistent setup across environments. - 📊 **Centralize Logs**: Stream to CloudWatch Logs or S3, and integrate with security platforms, or alternatively perform automated analysis on aggregated data, potentially using Generative AI. - 🧾 **Tune Policies**: Review and whitelist legitimate domains to avoid false positives - make the domain lists version controlled with say git. - 🔁 **Periodic Review**: Analyze logs regularly to refine firewall rules and detect new patterns.",
        "Getting more serious on DNS security after this article? Also read another article of mine:",
        "Turning on Route 53 Resolver query logging can transform DNS into a powerful diagnostic and security tool 🧠🔐. Paired with DNS Firewall, you gain both visibility and control—preventing unwanted traffic, uncovering phantom queries, and strengthening your AWS network posture.",
        "Stay curious, stay secure! 🛡️✨"
      ]
    },
    {
      "id": "article:fast-aws-console-navigation-with-chrome-site-search-b9e",
      "source_type": "article",
      "title": "Faster AWS Console Navigation with Chrome Site Search",
      "url": "https://gabrielkoo.com/blog/fast-aws-console-navigation-with-chrome-site-search-b9e/",
      "canonical_url": "https://dev.to/aws-builders/fast-aws-console-navigation-with-chrome-site-search-b9e",
      "published_at": "2025-02-11",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "cloud",
        "console"
      ],
      "description": "TLDR aws + [TAB] + ec2 + [ENTER] is all you need. No need to enter the entire AWS Console...",
      "content": "## TLDR\n\n`aws` + `[TAB]` + `ec2` + `[ENTER]` is all you need.\n\nNo need to enter the entire AWS Console URL every time, or consuming your energy in searching EC2’s icon in the console.\n\n## Background\n\nSo you may navigate different AWS console pages daily during work/project.\n\nSay you want to access the EFS service page in AWS Console, you typed in the URL bar but - Nope, no direct link since I didn't visit that page before on my browser.\n![Open EFS Console page](/assets/img/ac1b68a059ee.png)\n\nSo what if I want to navigate to EC2 service page?\nBetter, but still I have to do some cursor navigation.\n![Open EC2 Console page](/assets/img/810395bec601.png)\n\nIt takes extra attention for you to navigate to the right AWS Console's service page, even if you can type in the entire service name slug with your muscle memory 🧠. Typing the entire URL probably works, but it's more prone to errors.\n\n## Faster Way with Chrome Native Site Search\n\n1. Go to `chrome://settings/searchEngines` in Chrome\n2. Scroll down to `Site search`\n3. Click `Add`\n4. Fill in the following details:\n\n    Field | Value\n    ---|---\n    Name | `AWS Console`\n    Shortcut | `aws`\n    URL with %s in place of query | `https://console.aws.amazon.com/%s`\n   \n    ![Create site search](/assets/img/e31489221551.png)\n\n## Let's Try It\n\n![See it in action](/assets/img/e86aac802f23.gif)\n\nWith Chrome's Site Search feature, here's what required to navigate to the EC2 console page:\n\n1. Type `aws` in the URL bar\n2. Press `[SPACE]` or `[TAB]`\n3. enter the service name in lower case slug, e.g. `ec2`, `efs`, `ecs`, `s3`\n\nIt will redirect you to `console.aws.amazon.com/<service_name>`, which will in turn redirect you to `<you_default_region>.console.aws.amazon.com/<service_name>`.\n\nIt's quicker and much more handy, isn't it?",
      "excerpts": [
        "`aws` + `[TAB]` + `ec2` + `[ENTER]` is all you need.",
        "No need to enter the entire AWS Console URL every time, or consuming your energy in searching EC2’s icon in the console.",
        "So you may navigate different AWS console pages daily during work/project.",
        "Say you want to access the EFS service page in AWS Console, you typed in the URL bar but - Nope, no direct link since I didn't visit that page before on my browser. ![Open EFS Console page](/assets/img/ac1b68a059ee.png)",
        "So what if I want to navigate to EC2 service page? Better, but still I have to do some cursor navigation. ![Open EC2 Console page](/assets/img/810395bec601.png)",
        "It takes extra attention for you to navigate to the right AWS Console's service page, even if you can type in the entire service name slug with your muscle memory 🧠. Typing the entire URL probably works, but it's more prone to errors.",
        "Faster Way with Chrome Native Site Search",
        "1. Go to `chrome://settings/searchEngines` in Chrome 2. Scroll down to `Site search` 3. Click `Add` 4. Fill in the following details:",
        "Field | Value ---|--- Name | `AWS Console` Shortcut | `aws` URL with %s in place of query | `https://console.aws.amazon.com/%s` ![Create site search](/assets/img/e31489221551.png)",
        "![See it in action](/assets/img/e86aac802f23.gif)",
        "With Chrome's Site Search feature, here's what required to navigate to the EC2 console page:",
        "1. Type `aws` in the URL bar 2. Press `[SPACE]` or `[TAB]` 3. enter the service name in lower case slug, e.g. `ec2`, `efs`, `ecs`, `s3`",
        "It will redirect you to `console.aws.amazon.com/ `, which will in turn redirect you to ` .console.aws.amazon.com/ `.",
        "It's quicker and much more handy, isn't it?"
      ]
    },
    {
      "id": "article:scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm",
      "source_type": "article",
      "title": "Scale a Stateful Streamlit Chatbot with AWS ECS and EFS",
      "url": "https://gabrielkoo.com/blog/scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm/",
      "canonical_url": "https://dev.to/aws-builders/scale-a-stateful-streamlit-chatbot-with-aws-ecs-and-efs-48gm",
      "published_at": "2025-01-21",
      "last_verified_at": "2026-08-23",
      "tags": [
        "ai",
        "python",
        "aws",
        "streamlit"
      ],
      "description": "You built a great Streamlit application. Everything is working well locally when your boss...",
      "content": "You built a great **Streamlit** application. Everything is working well locally when your boss asked:\n\n> Your PoC GenAI app is great. Let's make it available to the entire company!\n\nYou deployed it into a virtual machine (maybe using an address like `http://12.34.56.78:8501`) and your colleagues rushed in with enthusiasm, the server got overloaded and restarted repeatedly. Some of their precious usage data was lost forever. They lost their work. You tried to duct tape fix it by doubling the size of the VM (repeatedly), but the same overload + restart pattern repeated. You feel helpless.\n\n## Introduction\n\n**Streamlit** is undoubtedly one of the greatest frameworks for Python developers to build interactive web apps. With the surge of interest in Generative AI since late 2022, the package has seen a significant rise in popularity, as evidenced by its growing number of GitHub stargazers:\n\n![Streamlit GitHub Repo Stargazers](/assets/img/21d0001a60b2.png)\n\nMore importantly, using Streamlit means you don't need to worry about learning the frontend side of things. You can focus on building the core functionality of your app, while Streamlit takes care of the rest. However, when it comes to deploying Streamlit apps to the cloud, things can get a bit tricky.\n\n## Statefulness and Scalability with Streamlit\n\nWhen you transition your Streamlit app from a local environment to the cloud, two critical challenges arise: ensuring **statefulness** and achieving **scalability**. By default, Streamlit maintains state **in-memory**, which means that any state is lost when the user **refreshes the page** or the **server restarts**. This can be a significant hurdle when scaling the application across multiple instances, or when answering your boss that _\"why don't you just share your local PoC GenAI application to other colleagues?\"_.\n\nI saw the Streamlit community has been discussing this multiple times ([here](https://discuss.streamlit.io/t/how-to-really-scale-a-streamlit-app/44662), [here](https://discuss.streamlit.io/t/scalability-how-well-does-streamlit-scale/68905), [here](https://discuss.streamlit.io/t/scaling-up-my-app/57846) and [here](https://discuss.streamlit.io/t/scalability-concerns-with-large-user-base/69494)), but so far I couldn't find any comprehensive guide on how to deploy a stateful Streamlit application to the cloud. This article aims to fill that gap by providing a detailed guide on deploying a **scalable and stateful Streamlit chatbot on AWS**.\n\n\n\n## Architecture\n\nTo overcome these challenges, this article introduces a scalable and stateful Streamlit chatbot deployed on AWS. The architecture leverages:\n\n- **Application Load Balancer (ALB):** Distributes incoming application traffic across multiple targets, ensuring even load distribution.\n- **Elastic Container Service (ECS) on Fargate:** Manages Docker containers, allowing for **easy scaling without the need to manage servers**. Using `arm64` and `0.25vCPU`/`0.5GB RAM` ECS Tasks for extra cost-performance efficiency.\n- **Elastic File System (EFS):** Provides a **scalable file system** that can be mounted to multiple ECS nodes, ensuring **data persistence and redundancy** across Availability Zones (AZs).\n- **CloudFront (optional):** Acts as a Content Delivery Network (CDN) to improve performance and reduce latency for users - more importantly **HTTPS**.\n\n![Architecture](/assets/img/2ed22e3e8d5e.png)\n\n### Not Lambda?\n\nI did consider using **Lambda** on either a Python or a Docker execution environment. However, Streamlit requires the websocket resource `/_stcore/stream` to be used. While services like **Amazon API Gateway** does support WebSocket, such an implementation requires you to define multiple Lambda handlers for websocket on **connect/data/disconnect events**, differing from the usual practices of e.g. using a Lambda Web Adaptor for the case of acting as a traditional HTTP endpoint.\n\nMore importantly, Streamlit's client frontend sends data to the websocket API as **binary frames**, while **Amazon API Gateway only supports text frames**. This makes it _impossible_ to use Lambda as a Streamlit backend.\n\n## Why Pick EFS But Not Others?\n\nWhile databases like RDS or DynamoDB, caching solutions like ElasticCache, and storage services like S3 are common choices for state management, they come with their own set of complexities and costs.\n\n| Option | Pros | Cons |\n|---|---|---|\n| RDS | Reliable, robust | Complex setup, high cost, item limits |\n| DynamoDB | Scalable, fast | Complex setup, high cost, item limits, manual binary serialization |\n| ElasticCache | Efficient caching | Complex setup, state loss on restarts, requires tunnel for local |\n| S3 | Cost effective | Network latency for get/set operations unless you pay for S3 VPC Gateway Endpoint |\n| EFS | Easy setup, scalable, persistent, cost effective | Cost/Latency starts to become an issue when scaled to enterprise level |\n\nEFS offers a **simpler and more cost-effective** solution. It provides a network file system that can be **mounted to multiple ECS nodes**, ensuring **redundancy across AZs and scalability**. It is easy to set up and relatively cheap, making it an ideal choice for persistent state storage. More importantly as **only file operations are involved**, **the difference in the local and cloud setups are minimal**.\n\n## You Mentioned Cost But Load Balancers Are Not Free\n\nIt's true that the Application Load Balancer (ALB) incurs _a fixed cost_. However, the benefits of using ALB — such as automatic distribution of incoming application traffic, support for HTTP/2, and integration with AWS services—outweigh the cost. Additionally, the scalability and reliability it provides are crucial for a **production-ready application**. The cost of ALB is justified by the enhanced performance and manageability it offers.\n\n## Why This Approach?\n\nDeploying a Streamlit app to the cloud requires careful consideration of both scalability and statefulness. By default, Streamlit’s **in-memory state management** is insufficient for a production environment for multiple users. Simply scaling up a virtual machine isn’t enough; you need a solution that persists user sessions across **refreshes and server restarts**.\n\nThe solution involves using the user's browser to store a session key in local storage with the `streamlit-local-storage` package, while each session is saved into a folder in the mounted EFS storage whose path is constructed with the session key, as Local storage isn't meant to be used to store too much binary data. This ensures that **session data is persistent and synced across multiple ECS nodes**.\n\nInstead of opting for **complex and costly database solutions** like RDS or DynamoDB, or dealing with the intricacies of ElasticCache, EFS provides a straightforward and efficient alternative. It allows for easy setup, scalability, and cost-effectiveness, making it the ideal choice for this deployment.\n\nThe most important beauty of this approach is that **your code will work the same across the cloud environment and your local one** - as the data persistency part is just **simple file read/write operations**. No hustle of setting up a local database.\n\n## A Project Template for Scalable Stateful Streamlit App\n\nIt's a LLM chatbot based on Amazon Bedrock, offers basic model switching and conversation reset. On the left panel, you can see the hostname of the ECS Task serving your latest Streamlit run, as well as your session ID.\n\nReferring to the screenshot below of the app you would see if you follow my instructions in the last section:\n\n![Image description](/assets/img/c62d9807ed58.png)\n\nI first started the conversation on the left window, started a new window (with the session key persisted in local storage), managed by retrieve back my conversation while the Streamlit run was made by another ECS Task as suggested by the hostname on the left panel. \n\n## Pseudo Code of the Streamlit Python Script\n\nHere’s a simplified pseudo code of the Python script used in the Streamlit application to manage session data:\n\n```python\nimport uuid\nimport pickle\nimport streamlit as st\nfrom streamlit_local_storage import LocalStorage\n... other imports ...\n\nlocal_storage = LocalStorage()\nsession_data = {}\nsession_id = local_storage.getItem('session_id') or str(uuid.uuid4())\nif session_id:\n    with open(f'/session_data/{session_id}.json', 'r') as f:\n        for key, value in pickle.load(f):\n            session_data[key] = value\n\nsession_data['some-key'] = st.some_input(label='Enter some input here')\n... main chatbot logic here ...\n\nwith open(f'/session_data/{session_id}.json', 'w') as f:\n    pickle.dump(session_data, f)\n\n```\n\nIn this script:\n\n- A `session_data` local singleton dictionary is used to store session data.\n- A `session_id` is generated or retrieved from local storage.\n- The session data is loaded from the file system based on the `session_id` during script initialization,\n  while the session data is saved back to the file system at the end of the script.\n- As EFS is mounted to all ECS nodes, the session data is shared across all ECS Task instances, surviving across scaling activities even when a separate ECS Task is used to serve your existing Streamlit session.\n\nStreamlit's native `session_state` isn't used in my approach, as it is **in-memory** and **not shared across multiple ECS nodes**. Under a auto scaling environment, it is possible for every ECS Task to have served each user session at some point, making the session data available in-memory could lead to data inconsistency, as well as memory exhaustion. The current approach only requires the session key to be stored in-memory, which is a negligible amount of data.\n\n## Deploy It For Your Organization\n\nTo deploy this scalable and stateful Streamlit chatbot on AWS, follow these steps:\n\n1. **Clone my Repository:** Start by cloning my repository: \n   <https://github.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws>\n   to your local machine.\n2. **Deploy the CloudFormation Stack:** Use the [`template.yml`](https://raw.githubusercontent.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws/refs/heads/main/template.yml) file to deploy the necessary AWS infrastructure. It’s recommended to use the AWS Management Console for an intuitive setup.\n3. **Build and Deploy the Docker Image:** Run `./deployment.sh` to:\n    - build the Docker image\n    - push it to Amazon Elastic Container Registry (ECR)\n    - scales up ECS service with the new image\n4. **Access the Chatbot:** Once deployed, access the chatbot via the URL provided by the Application Load Balancer (ALB) or through the CloudFront URL for added performance and HTTPS support.\n5. **Enable Auto Scaling:** To truly leverage the scalability of this setup, configure Auto Scaling for the ECS service. This step ensures that your application can handle varying loads efficiently.\n\n> Note: My repo did not cover step #5.\n\nBy following these steps, you can deploy a **robust, scalable, and stateful Streamlit application** on AWS, ensuring a seamless user experience even under heavy load. This approach also provides a cost-effective and efficient solution for deploying Streamlit applications in a production environment, as compared to the \"just double your virtual machine\" brainless method.\n\nMore importantly, you can focus on building great GenAI applications with Streamlit locally and just scale it without a headache on AWS!",
      "excerpts": [
        "You built a great **Streamlit** application. Everything is working well locally when your boss asked:",
        "> Your PoC GenAI app is great. Let's make it available to the entire company!",
        "You deployed it into a virtual machine (maybe using an address like `http://12.34.56.78:8501`) and your colleagues rushed in with enthusiasm, the server got overloaded and restarted repeatedly. Some of their precious usage data was lost forever. They lost their work. You tried to duct tape fix it by doubling the size of the VM (repeatedly), but the same overload + restart pattern repeated. You feel helpless.",
        "**Streamlit** is undoubtedly one of the greatest frameworks for Python developers to build interactive web apps. With the surge of interest in Generative AI since late 2022, the package has seen a significant rise in popularity, as evidenced by its growing number of GitHub stargazers:",
        "![Streamlit GitHub Repo Stargazers](/assets/img/21d0001a60b2.png)",
        "More importantly, using Streamlit means you don't need to worry about learning the frontend side of things. You can focus on building the core functionality of your app, while Streamlit takes care of the rest. However, when it comes to deploying Streamlit apps to the cloud, things can get a bit tricky.",
        "Statefulness and Scalability with Streamlit",
        "When you transition your Streamlit app from a local environment to the cloud, two critical challenges arise: ensuring **statefulness** and achieving **scalability**. By default, Streamlit maintains state **in-memory**, which means that any state is lost when the user **refreshes the page** or the **server restarts**. This can be a significant hurdle when scaling the application across multiple instances, or when answering your boss that _\"why don't you just share your local PoC GenAI application to other colleagues?\"_.",
        "I saw the Streamlit community has been discussing this multiple times ([here](https://discuss.streamlit.io/t/how-to-really-scale-a-streamlit-app/44662), [here](https://discuss.streamlit.io/t/scalability-how-well-does-streamlit-scale/68905), [here](https://discuss.streamlit.io/t/scaling-up-my-app/57846) and [here](https://discuss.streamlit.io/t/scalability-concerns-with-large-user-base/69494)), but so far I couldn't find any comprehensive guide on how to deploy a stateful Streamlit application to the cloud. This article aims to fill that gap by providing a detailed guide on deploying a **scalable and stateful Streamlit chatbot on AWS**.",
        "To overcome these challenges, this article introduces a scalable and stateful Streamlit chatbot deployed on AWS. The architecture leverages:",
        "- **Application Load Balancer (ALB):** Distributes incoming application traffic across multiple targets, ensuring even load distribution. - **Elastic Container Service (ECS) on Fargate:** Manages Docker containers, allowing for **easy scaling without the need to manage servers**. Using `arm64` and `0.25vCPU`/`0.5GB RAM` ECS Tasks for extra cost-performance efficiency. - **Elastic File System (EFS):** Provides a **scalable file system** that can be mounted to multiple ECS nodes, ensuring **data persistence and redundancy** across Availability Zones (AZs). - **CloudFront (optional):** Acts as a Content Delivery Network (CDN) to improve performance and reduce latency for users - more importantly **HTTPS**.",
        "![Architecture](/assets/img/2ed22e3e8d5e.png)",
        "I did consider using **Lambda** on either a Python or a Docker execution environment. However, Streamlit requires the websocket resource `/_stcore/stream` to be used. While services like **Amazon API Gateway** does support WebSocket, such an implementation requires you to define multiple Lambda handlers for websocket on **connect/data/disconnect events**, differing from the usual practices of e.g. using a Lambda Web Adaptor for the case of acting as a traditional HTTP endpoint.",
        "More importantly, Streamlit's client frontend sends data to the websocket API as **binary frames**, while **Amazon API Gateway only supports text frames**. This makes it _impossible_ to use Lambda as a Streamlit backend.",
        "While databases like RDS or DynamoDB, caching solutions like ElasticCache, and storage services like S3 are common choices for state management, they come with their own set of complexities and costs.",
        "| Option | Pros | Cons | |---|---|---| | RDS | Reliable, robust | Complex setup, high cost, item limits | | DynamoDB | Scalable, fast | Complex setup, high cost, item limits, manual binary serialization | | ElasticCache | Efficient caching | Complex setup, state loss on restarts, requires tunnel for local | | S3 | Cost effective | Network latency for get/set operations unless you pay for S3 VPC Gateway Endpoint | | EFS | Easy setup, scalable, persistent, cost effective | Cost/Latency starts to become an issue when scaled to enterprise level |",
        "EFS offers a **simpler and more cost-effective** solution. It provides a network file system that can be **mounted to multiple ECS nodes**, ensuring **redundancy across AZs and scalability**. It is easy to set up and relatively cheap, making it an ideal choice for persistent state storage. More importantly as **only file operations are involved**, **the difference in the local and cloud setups are minimal**.",
        "You Mentioned Cost But Load Balancers Are Not Free",
        "It's true that the Application Load Balancer (ALB) incurs _a fixed cost_. However, the benefits of using ALB — such as automatic distribution of incoming application traffic, support for HTTP/2, and integration with AWS services—outweigh the cost. Additionally, the scalability and reliability it provides are crucial for a **production-ready application**. The cost of ALB is justified by the enhanced performance and manageability it offers.",
        "Deploying a Streamlit app to the cloud requires careful consideration of both scalability and statefulness. By default, Streamlit’s **in-memory state management** is insufficient for a production environment for multiple users. Simply scaling up a virtual machine isn’t enough; you need a solution that persists user sessions across **refreshes and server restarts**.",
        "The solution involves using the user's browser to store a session key in local storage with the `streamlit-local-storage` package, while each session is saved into a folder in the mounted EFS storage whose path is constructed with the session key, as Local storage isn't meant to be used to store too much binary data. This ensures that **session data is persistent and synced across multiple ECS nodes**.",
        "Instead of opting for **complex and costly database solutions** like RDS or DynamoDB, or dealing with the intricacies of ElasticCache, EFS provides a straightforward and efficient alternative. It allows for easy setup, scalability, and cost-effectiveness, making it the ideal choice for this deployment.",
        "The most important beauty of this approach is that **your code will work the same across the cloud environment and your local one** - as the data persistency part is just **simple file read/write operations**. No hustle of setting up a local database.",
        "A Project Template for Scalable Stateful Streamlit App",
        "It's a LLM chatbot based on Amazon Bedrock, offers basic model switching and conversation reset. On the left panel, you can see the hostname of the ECS Task serving your latest Streamlit run, as well as your session ID.",
        "Referring to the screenshot below of the app you would see if you follow my instructions in the last section:",
        "![Image description](/assets/img/c62d9807ed58.png)",
        "I first started the conversation on the left window, started a new window (with the session key persisted in local storage), managed by retrieve back my conversation while the Streamlit run was made by another ECS Task as suggested by the hostname on the left panel.",
        "Pseudo Code of the Streamlit Python Script",
        "Here’s a simplified pseudo code of the Python script used in the Streamlit application to manage session data:",
        "local_storage = LocalStorage() session_data = {} session_id = local_storage.getItem('session_id') or str(uuid.uuid4()) if session_id: with open(f'/session_data/{session_id}.json', 'r') as f: for key, value in pickle.load(f): session_data[key] = value",
        "session_data['some-key'] = st.some_input(label='Enter some input here') ... main chatbot logic here ...",
        "with open(f'/session_data/{session_id}.json', 'w') as f: pickle.dump(session_data, f)",
        "- A `session_data` local singleton dictionary is used to store session data. - A `session_id` is generated or retrieved from local storage. - The session data is loaded from the file system based on the `session_id` during script initialization, while the session data is saved back to the file system at the end of the script. - As EFS is mounted to all ECS nodes, the session data is shared across all ECS Task instances, surviving across scaling activities even when a separate ECS Task is used to serve your existing Streamlit session.",
        "Streamlit's native `session_state` isn't used in my approach, as it is **in-memory** and **not shared across multiple ECS nodes**. Under a auto scaling environment, it is possible for every ECS Task to have served each user session at some point, making the session data available in-memory could lead to data inconsistency, as well as memory exhaustion. The current approach only requires the session key to be stored in-memory, which is a negligible amount of data.",
        "Deploy It For Your Organization",
        "To deploy this scalable and stateful Streamlit chatbot on AWS, follow these steps:",
        "1. **Clone my Repository:** Start by cloning my repository: to your local machine. 2. **Deploy the CloudFormation Stack:** Use the [`template.yml`](https://raw.githubusercontent.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws/refs/heads/main/template.yml) file to deploy the necessary AWS infrastructure. It’s recommended to use the AWS Management Console for an intuitive setup. 3. **Build and Deploy the Docker Image:** Run `./deployment.sh` to: - build the Docker image - push it to Amazon Elastic Container Registry (ECR) - scales up ECS service with the new image 4. **Access the Chatbot:** Once deployed, access the chatbot via the URL provided by the Application Load Balancer (ALB) or through the CloudFront URL for added performance and HTTPS support. 5. **Enable Auto Scaling:** To truly leverage the scalability of this setup, configure Auto Scaling for the ECS service. This step ensures that your application can handle varying loads efficiently.",
        "> Note: My repo did not cover step #5.",
        "By following these steps, you can deploy a **robust, scalable, and stateful Streamlit application** on AWS, ensuring a seamless user experience even under heavy load. This approach also provides a cost-effective and efficient solution for deploying Streamlit applications in a production environment, as compared to the \"just double your virtual machine\" brainless method.",
        "More importantly, you can focus on building great GenAI applications with Streamlit locally and just scale it without a headache on AWS!"
      ]
    },
    {
      "id": "article:use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5",
      "source_type": "article",
      "title": "Use Amazon Bedrock Models with OpenAI SDKs with a Serverless Proxy Endpoint - Without Fixed Cost!",
      "url": "https://gabrielkoo.com/blog/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5/",
      "canonical_url": "https://dev.to/aws-builders/use-amazon-bedrock-models-via-an-openai-api-compatible-serverless-endpoint-now-without-fixed-cost-5hf5",
      "published_at": "2025-01-02",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "openai",
        "serverless",
        "bedrock"
      ],
      "description": "UPDATE (2025-08-05): AWS silently launched an official OpenAI compatible API endpoint - but so far it...",
      "content": "**UPDATE (2026-02-26)**: Support for Nova 2 multimodal Embeddings is supported after [my PR is merged](https://github.com/aws-samples/bedrock-access-gateway/pull/222)! \n**UPDATE (2025-08-05)**: AWS silently launched an [official OpenAI compatible API endpoint](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html) - but so far it only supports the `gpt-oss` reasoning models and doesn't support tool calls (refer to my [new article](https://dev.to/aws-builders/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd)). So this solution still rocks until AWS gradually adds more support.\n\n## Why `bedrock-access-gateway-function-url`\n\nThis article is for GenAI builders who cares for all of these:\n\n0. I want to use OpenAI SDK/compatible API\n1. No fixed cost, pay as you go priced \n2. Serverless LLM, no self hosting\n3. Multiple models in one codebase\n4. Lightweight solution without bloatware\n\nIf you want to stick to AWS, and wants to keep the simplicity without the burden of maintain extra configuration, continue reading.\n\n## Why not XXX as solution instead?\n\n<table>\n  <thead>\n    <tr>\n      <th>Solution</th>\n      <th>Pros & Cons</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><strong>LiteLLM</strong> (SDK)</td>\n      <td>\n        (-) <a href=\"https://github.com/BerriAI/litellm/blob/d77b825814c354935ac540c8f8b4b696f23d83c9/pyproject.toml#L21-L61\">full list of unnecessary dependencies</a> potentially bloating your Python environment/application, e.g., <code>gunicorn</code>, <code>fastapi</code>, <code>google-cloud-kms</code>, etc.<br>\n        (-) Python only\n      </td>\n    </tr>\n    <tr>\n      <td><strong>LiteLLM</strong> (Proxy)</td>\n      <td>\n        (-) Huge infra cost (Worker + Database + Redis)<br>\n        (-) <a href=\"https://docs.litellm.ai/docs/proxy/deploy#platform-specific-guide\">Good luck with maintaining <code>docker-compose</code> / K8S</a>\n      </td>\n    </tr>\n    <tr>\n      <td><strong>bedrock-access-gateway</strong></td>\n      <td>\n        (-) >US$16/month<br>\n        (-) Extra Load Balancer needed + Fargate/Lambda pricing\n      </td>\n    </tr>\n    <tr>\n      <td><strong>aisuite</strong></td>\n      <td>\n        (+) No bloatware issue with usage of <code>extra</code> Python dependencies<br>\n        (+) No extra infra cost<br>\n        (-) Python Only\n      </td>\n    </tr>\n    <tr>\n      <td><strong>This Solution</strong></td>\n      <td>\n        (+) Only minimal pay-as-you-go Lambda exec costs<br>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\n## A Typical GenAI Builder's Struggle\n\nYou are a builder specialized on AWS, maybe with a lot of AWS Credits like me.\n\nYou want to build GenAI applications when you found that most starters/examples are based on OpenAI's official Python/NodeJS SDKs, [e.g.](https://platform.openai.com/docs/api-reference/chat/create?lang=python):\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI()\n\ncompletion = client.chat.completions.create(\n    model=\"gpt-3.5-turbo\",\n    messages=[\n        {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n        {\"role\": \"user\", \"content\": \"Hello!\"}\n    ]\n)\n\nprint(completion.choices[0].message)\n# > Hello! What can I help you?\n```\n\nOr separately, you have a bunch of AWS Credits that would be useful for you to utilize them with e.g. a coding CLI agent, but Amazon Q Developer CLI ($19/month) is too big of a spending for you - you might want to use OpenAI Codex CLI instead - <https://dev.to/aws-builders/use-openai-codex-cli-with-amazon-bedrock-models-pay-as-you-go-48eb>.\n\nIf you did read through Amazon Bedrock docs, you would realize that the data schema of the Bedrock Runtime Converse API for chat completions is very different from OpenAI's. If you need to allow model/provider switching in your GenAI application, this is particularly a burden because you might need to write very different implementations for each provider.\n\nThere are also other provider specific implementations: VertexAI, Gemini API, LangChain, etc. It takes effort to rewrite your code to cater for more models. If you are working on multiple projects, you might be maintaining the same set of code within different projects.\n\n## A New Hope - But It Comes with a Fixed Cost\n\nTo fix this issue, AWS has provided the great project [`aws-samples/bedrock-access-gateway`](https://github.com/aws-samples/bedrock-access-gateway) - It allows you to deploy an `Application Load Balancer + Lambda/Fargate` pair so that you can use OpenAI's official SDKs with the OpenAI-API compatibile Rest API endpoint via the environment variables `OPENAI_API_BASE` and `OPENAI_API_KEY`.\n\nIt achieves goals #2 and #3 in the first section. You can work on projects utilizing OpenAI SDKs with ease.\n\n## The Fixed Cost Strikes Back\n\nYes it's absolutely great, but it's also costly if you are building your GenAI project particularly with your own money/limited budget:\n\n1. Application Load Balancer is running 24/7 once deployed, it comes with a fixed cost per hour:\n  - $0.0225 per Application Load Balancer-hour; or\n  - $16.2 / month _FIXED_ cost regardless of usage\n  - In addition, there is also the variable cost:\n    `No. LCUs used * $0.008 per LCU-hour`\n\n2. Fargate (the alternative deployment option) is also running 24/7, so it also comes with an additional fixed cost on top of ALB:\n  - $0.04048 / vCPU hour\n  - $0.004445 / GB hour\n  - $35.5 / month _FIXED_ cost under the default 1vCPU+2GB RAM setup\n\nIt's a cost nightmare especially for those who don't require 24/7 uptime and usage for the OpenAI compatible API endpoint.\n\nAlso if a fixed cost is unavoidable, why don’t we just start a cloud VM and put everything inside it instead?\n\n## Why Bedrock in the First Place?\n\nSomething feels wrong to me. I used Amazon Bedrock with the 1st reason being it's **serverless** nature and pay as you go capability - Why bother to pay a gigantic fixed monthly cost to host your own open sourced LLM with a VM paired with expensive GPU when you can just pick the serverless option?\n\nThe 2nd reason of picking Bedrock is on the ease of switching models.\n\nWith Bedrock, not only you can use proprietary models like Amazon Nova, but also it's immediate compatibility with other open source models like LLaMA 3.3 (While VertexAI is still offering LLaMA 3.2 at most) or Mistral by just changing the `model` field in your code - without extra “endpoint deployments” - this is what other major Cloud AI providers can't provide at the moment.\n\nFor example for Azure AI, every non-OpenAI model needs to be deployed into separate inference endpoints:\n\n```python\nimport os\nfrom azure.ai.inference import ChatCompletionsClient\nfrom azure.core.credentials import AzureKeyCredential\n\nmodel_a = ChatCompletionsClient(\n    endpoint=os.environ[\"AZUREAI_ENDPOINT_URL_A\"],\n    credential=AzureKeyCredential(os.environ[\"AZUREAI_ENDPOINT_KEY_A\"]),\n)\nmodel_b = ChatCompletionsClient(\n    endpoint=os.environ[\"AZUREAI_ENDPOINT_URL_B\"],\n    credential=AzureKeyCredential(os.environ[\"AZUREAI_ENDPOINT_KEY_B\"]),\n)\n```\n\nFor VertexAI, while you can use non-Gemini models with the same API endpoint as well as credential, the OpenAI API compatible endpoint by Google Cloud [is still in beta](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library?hl=en) as of time of writing (2025 Jan) - as well as [multiple users are still reporting issues with tool calling](https://discuss.ai.google.dev/t/gemini-openai-compatibility-multiple-functions-support-in-function-calling-error-400/49431).\n\nAgain, I want to stick with Bedrock with my OpenAI SDKs, but I am not willing to pay a fixed recurring cost for my GenAI application that might not generate 24/7 traffic.\n\n## When in Doubt, Read the Docs First\n\nThe maintainers of `bedrock-access-gateway` [suggested](https://github.com/aws-samples/bedrock-access-gateway?tab=readme-ov-file#any-performance-sacrifice-or-latency-increase-by-using-the-proxy-apis), namely for performance improvements that:\n\n> Also, you can use Lambda Web Adapter + Function URL (see [example](https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming)) to replace ALB or AWS Fargate to replace Lambda to get better performance on streaming response.\n\nWe could have used a [Lambda Function URL to replace ALB via Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming).\n\nThis sample app provided by AWS was based on a Lambda function with a Docker runtime, and as it's name suggests, it is a sample app not used for general purposes: `Serverless Bedtime Storyteller`. With this example in place, I can build a serverless \"Fixed Cost lessness\" version of the bedrock access gateway.\n\n## Building the `bedrock-access-gateway-function-url` project\n\nI made a few tweaks from the original `bedrock-access-gateway` project:\n\n1. Ditched [`Magnum`](https://pypi.org/project/mangum/) for [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter)\n2. Switched from a Docker Container runtime back to Python runtime with layers - as Lambda Docker runtimes are famous for it's cold start times\n3. Enabled the option (`--no-embedding`) to exclude embedding related dependencies which could drastically increase the build size - `tiktoken` and `numpy`\n4. Wrapped the Python handler with a custom entry point `run.sh`\n  \n  ```shell\n  #!/bin/bash\n  PATH=$PATH:$LAMBDA_TASK_ROOT/bin \\\n    PYTHONPATH=$LAMBDA_TASK_ROOT:$PYTHONPATH:/opt/python \\\n    exec python3 api/app.py\n  ```\n  \n  This is [necessary](https://github.com/awslabs/aws-lambda-web-adapter/issues/441) since using the Lambda Web Adapter resets some Python Path settings which would cause your Layered dependencies to be un-importable.\n\nLastly, the crux of my project is the very `prepare_source.sh` file - it fetches the latest Python source of `bedrock-access-gateway` with `git` so that the latest efforts from the `aws-examples` contributors are included. The scripts clones from the latest `main` branch of the project, and copies the Python FastAPI implementation of the access gateway.\n\nIt also conducts an optional dependency reduction if you do not need to call the embeddings endpoint, as large PyPI dependencies like `numpy` or `tiktoken` could have been avoided.\n\n## Deployment\n\nStraightforward. I personally recommend using the AWS CloudShell as you can even do so with your mobile AWS Console, and you can save some time by skipping the need of a Docker build:\n\n```shell\nsudo yum update -y\nsudo yum install -y python3.12 python3.12-pip\n\ngit clone --depth=1 https://github.com/gabrielkoo/bedrock-access-gateway-function-url\ncd bedrock-access-gateway-function-url\n\n./prepare_source.sh\nsam build\nsam deploy --guided\n```\n\nAfter within a minute, grab the value of `FunctionUrl` as well as recall the value of `ApiKey` value you supplied earlier in `sam deploy`:\n\n```\nOutputs                                                                                                                                                                                                                                       \n\nKey                 Function                                                                                                                                                                                                                  \nDescription         FastAPI Lambda Function ARN                                                                                                                                                                                               \nValue               arn:aws:lambda:us-east-1:123456789012:function:sam-app-BedrockAccessGatewayFunction-yLLzetPaKSq5                                                                                                                          \n\nKey                 FunctionUrl                                                                                                                                                                                                               \nDescription         Function URL for FastAPI function                                                                                                                                                                                         \nValue               https://lukeskywalker.lambda-url.us-east-1.on.aws/                                                                                                                                                     \n\nSuccessfully created/updated stack - sam-app in us-east-1\n```\n\nNow, test your own dedicated pay-as-you-go serverless infrastructure OpenAI-compatible API endpoint in your GenAI application!\n\n```shell\ncurl \"${FUNCTION_URL}api/v1/models\" \\\n     -H \"Authorization: Bearer $API_KEY\"\n# {\n#   \"object\": \"list\",\n#   \"data\": [\n#     {\n#       \"id\": \"amazon.titan-tg1-large\",\n#       \"created\": 1735826872,\n#       \"object\": \"model\",\n#       \"owned_by\": \"bedrock\"\n#     },\n#     ...\n#   ]\n# }\n```\n\n![Streaming with the Access Gateway](/assets/img/8c968f3c435e.gif)\n\nAlternatively, I have built a minimal UI based on the `deep-chat` project so that you can test it without access to any local shell environment: <https://chat.gab.hk/>.\n\nNo worries about security - it’s an open sourced static website, no backend and tracking scripts. Just bring your own endpoint and key.\n\n## Return of Cost Effectiveness\n\nWith the new true serverless option, here are the costs incurred:\n\n- Amazon Bedrock costs: Pay-as-you-go according to token usage\n- Lambda Invocation costs: Per GB-second + Per Requests\n\nSo here is the final repository containing the entire setup:\n\n<https://github.com/gabrielkoo/bedrock-access-gateway-function-url>\n\nFeel free to fork it and create your own!\n\n## Next Steps\n\nIn order to further productionize it, here a list of to-dos that could have been done:\n\n1. Wrap the Function URL with Amazon CloudFront and adopt OAC - Reference Article - [Secure your Lambda function URLs using Amazon CloudFront origin access control](https://aws.amazon.com/blogs/networking-and-content-delivery/secure-your-lambda-function-urls-using-amazon-cloudfront-origin-access-control/)\n2. Experiment for the optimal memory size and timeout for the Lambda handler to achieve better cost efficiency\n3. Use provisioned throughput to further avoid Lambda cold starts\n4. Support multiple API keys by updating `api.auth.api_key_auth` logic\n5. Support non-text/image content, such as `DocumentContent` or `VideoContent` which are [well supported by Amazon Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html).\n\n## Credits\n\nSpecial thanks to the contributors of the following two projects. As without their efforts, this cost effective gateway won't even exist:\n\n- [aws-samples/bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway)\n- [awslabs/aws-lambda-web-adapter](https://github.com/awslabs/aws-lambda-web-adapter)",
      "excerpts": [
        "**UPDATE (2026-02-26)**: Support for Nova 2 multimodal Embeddings is supported after [my PR is merged](https://github.com/aws-samples/bedrock-access-gateway/pull/222)! **UPDATE (2025-08-05)**: AWS silently launched an [official OpenAI compatible API endpoint](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html) - but so far it only supports the `gpt-oss` reasoning models and doesn't support tool calls (refer to my [new article](https://dev.to/aws-builders/aws-launches-openai-compatible-api-for-bedrock-and-i-did-some-tests-49cd)). So this solution still rocks until AWS gradually adds more support.",
        "Why `bedrock-access-gateway-function-url`",
        "This article is for GenAI builders who cares for all of these:",
        "0. I want to use OpenAI SDK/compatible API 1. No fixed cost, pay as you go priced 2. Serverless LLM, no self hosting 3. Multiple models in one codebase 4. Lightweight solution without bloatware",
        "If you want to stick to AWS, and wants to keep the simplicity without the burden of maintain extra configuration, continue reading.",
        "Why not XXX as solution instead?",
        "Solution Pros & Cons LiteLLM (SDK) (-) full list of unnecessary dependencies potentially bloating your Python environment/application, e.g., gunicorn , fastapi , google-cloud-kms , etc. (-) Python only LiteLLM (Proxy) (-) Huge infra cost (Worker + Database + Redis) (-) Good luck with maintaining docker-compose / K8S bedrock-access-gateway (-) >US$16/month (-) Extra Load Balancer needed + Fargate/Lambda pricing aisuite (+) No bloatware issue with usage of extra Python dependencies (+) No extra infra cost (-) Python Only This Solution (+) Only minimal pay-as-you-go Lambda exec costs",
        "A Typical GenAI Builder's Struggle",
        "You are a builder specialized on AWS, maybe with a lot of AWS Credits like me.",
        "You want to build GenAI applications when you found that most starters/examples are based on OpenAI's official Python/NodeJS SDKs, [e.g.](https://platform.openai.com/docs/api-reference/chat/create?lang=python):",
        "completion = client.chat.completions.create( model=\"gpt-3.5-turbo\", messages=[ {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}, {\"role\": \"user\", \"content\": \"Hello!\"} ] )",
        "print(completion.choices[0].message) > Hello! What can I help you? ```",
        "Or separately, you have a bunch of AWS Credits that would be useful for you to utilize them with e.g. a coding CLI agent, but Amazon Q Developer CLI ($19/month) is too big of a spending for you - you might want to use OpenAI Codex CLI instead - .",
        "If you did read through Amazon Bedrock docs, you would realize that the data schema of the Bedrock Runtime Converse API for chat completions is very different from OpenAI's. If you need to allow model/provider switching in your GenAI application, this is particularly a burden because you might need to write very different implementations for each provider.",
        "There are also other provider specific implementations: VertexAI, Gemini API, LangChain, etc. It takes effort to rewrite your code to cater for more models. If you are working on multiple projects, you might be maintaining the same set of code within different projects.",
        "A New Hope - But It Comes with a Fixed Cost",
        "To fix this issue, AWS has provided the great project [`aws-samples/bedrock-access-gateway`](https://github.com/aws-samples/bedrock-access-gateway) - It allows you to deploy an `Application Load Balancer + Lambda/Fargate` pair so that you can use OpenAI's official SDKs with the OpenAI-API compatibile Rest API endpoint via the environment variables `OPENAI_API_BASE` and `OPENAI_API_KEY`.",
        "It achieves goals #2 and #3 in the first section. You can work on projects utilizing OpenAI SDKs with ease.",
        "Yes it's absolutely great, but it's also costly if you are building your GenAI project particularly with your own money/limited budget:",
        "1. Application Load Balancer is running 24/7 once deployed, it comes with a fixed cost per hour: - $0.0225 per Application Load Balancer-hour; or - $16.2 / month _FIXED_ cost regardless of usage - In addition, there is also the variable cost: `No. LCUs used * $0.008 per LCU-hour`",
        "2. Fargate (the alternative deployment option) is also running 24/7, so it also comes with an additional fixed cost on top of ALB: - $0.04048 / vCPU hour - $0.004445 / GB hour - $35.5 / month _FIXED_ cost under the default 1vCPU+2GB RAM setup",
        "It's a cost nightmare especially for those who don't require 24/7 uptime and usage for the OpenAI compatible API endpoint.",
        "Also if a fixed cost is unavoidable, why don’t we just start a cloud VM and put everything inside it instead?",
        "Why Bedrock in the First Place?",
        "Something feels wrong to me. I used Amazon Bedrock with the 1st reason being it's **serverless** nature and pay as you go capability - Why bother to pay a gigantic fixed monthly cost to host your own open sourced LLM with a VM paired with expensive GPU when you can just pick the serverless option?",
        "The 2nd reason of picking Bedrock is on the ease of switching models.",
        "With Bedrock, not only you can use proprietary models like Amazon Nova, but also it's immediate compatibility with other open source models like LLaMA 3.3 (While VertexAI is still offering LLaMA 3.2 at most) or Mistral by just changing the `model` field in your code - without extra “endpoint deployments” - this is what other major Cloud AI providers can't provide at the moment.",
        "For example for Azure AI, every non-OpenAI model needs to be deployed into separate inference endpoints:",
        "model_a = ChatCompletionsClient( endpoint=os.environ[\"AZUREAI_ENDPOINT_URL_A\"], credential=AzureKeyCredential(os.environ[\"AZUREAI_ENDPOINT_KEY_A\"]), ) model_b = ChatCompletionsClient( endpoint=os.environ[\"AZUREAI_ENDPOINT_URL_B\"], credential=AzureKeyCredential(os.environ[\"AZUREAI_ENDPOINT_KEY_B\"]), ) ```",
        "For VertexAI, while you can use non-Gemini models with the same API endpoint as well as credential, the OpenAI API compatible endpoint by Google Cloud [is still in beta](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library?hl=en) as of time of writing (2025 Jan) - as well as [multiple users are still reporting issues with tool calling](https://discuss.ai.google.dev/t/gemini-openai-compatibility-multiple-functions-support-in-function-calling-error-400/49431).",
        "Again, I want to stick with Bedrock with my OpenAI SDKs, but I am not willing to pay a fixed recurring cost for my GenAI application that might not generate 24/7 traffic.",
        "When in Doubt, Read the Docs First",
        "The maintainers of `bedrock-access-gateway` [suggested](https://github.com/aws-samples/bedrock-access-gateway?tab=readme-ov-file#any-performance-sacrifice-or-latency-increase-by-using-the-proxy-apis), namely for performance improvements that:",
        "> Also, you can use Lambda Web Adapter + Function URL (see [example](https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming)) to replace ALB or AWS Fargate to replace Lambda to get better performance on streaming response.",
        "We could have used a [Lambda Function URL to replace ALB via Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming).",
        "This sample app provided by AWS was based on a Lambda function with a Docker runtime, and as it's name suggests, it is a sample app not used for general purposes: `Serverless Bedtime Storyteller`. With this example in place, I can build a serverless \"Fixed Cost lessness\" version of the bedrock access gateway.",
        "Building the `bedrock-access-gateway-function-url` project",
        "I made a few tweaks from the original `bedrock-access-gateway` project:",
        "1. Ditched [`Magnum`](https://pypi.org/project/mangum/) for [Lambda Web Adapter](https://github.com/awslabs/aws-lambda-web-adapter) 2. Switched from a Docker Container runtime back to Python runtime with layers - as Lambda Docker runtimes are famous for it's cold start times 3. Enabled the option (`--no-embedding`) to exclude embedding related dependencies which could drastically increase the build size - `tiktoken` and `numpy` 4. Wrapped the Python handler with a custom entry point `run.sh` ```shell #!/bin/bash PATH=$PATH:$LAMBDA_TASK_ROOT/bin \\ PYTHONPATH=$LAMBDA_TASK_ROOT:$PYTHONPATH:/opt/python \\ exec python3 api/app.py ``` This is [necessary](https://github.com/awslabs/aws-lambda-web-adapter/issues/441) since using the Lambda Web Adapter resets some Python Path settings which would cause your Layered dependencies to be un-importable.",
        "Lastly, the crux of my project is the very `prepare_source.sh` file - it fetches the latest Python source of `bedrock-access-gateway` with `git` so that the latest efforts from the `aws-examples` contributors are included. The scripts clones from the latest `main` branch of the project, and copies the Python FastAPI implementation of the access gateway.",
        "It also conducts an optional dependency reduction if you do not need to call the embeddings endpoint, as large PyPI dependencies like `numpy` or `tiktoken` could have been avoided.",
        "Straightforward. I personally recommend using the AWS CloudShell as you can even do so with your mobile AWS Console, and you can save some time by skipping the need of a Docker build:",
        "git clone --depth=1 https://github.com/gabrielkoo/bedrock-access-gateway-function-url cd bedrock-access-gateway-function-url",
        "./prepare_source.sh sam build sam deploy --guided ```",
        "After within a minute, grab the value of `FunctionUrl` as well as recall the value of `ApiKey` value you supplied earlier in `sam deploy`:",
        "Key Function Description FastAPI Lambda Function ARN Value arn:aws:lambda:us-east-1:123456789012:function:sam-app-BedrockAccessGatewayFunction-yLLzetPaKSq5",
        "Key FunctionUrl Description Function URL for FastAPI function Value https://lukeskywalker.lambda-url.us-east-1.on.aws/",
        "Successfully created/updated stack - sam-app in us-east-1 ```",
        "Now, test your own dedicated pay-as-you-go serverless infrastructure OpenAI-compatible API endpoint in your GenAI application!",
        "![Streaming with the Access Gateway](/assets/img/8c968f3c435e.gif)",
        "Alternatively, I have built a minimal UI based on the `deep-chat` project so that you can test it without access to any local shell environment: .",
        "No worries about security - it’s an open sourced static website, no backend and tracking scripts. Just bring your own endpoint and key.",
        "With the new true serverless option, here are the costs incurred:",
        "- Amazon Bedrock costs: Pay-as-you-go according to token usage - Lambda Invocation costs: Per GB-second + Per Requests",
        "So here is the final repository containing the entire setup:",
        "Feel free to fork it and create your own!",
        "In order to further productionize it, here a list of to-dos that could have been done:",
        "1. Wrap the Function URL with Amazon CloudFront and adopt OAC - Reference Article - [Secure your Lambda function URLs using Amazon CloudFront origin access control](https://aws.amazon.com/blogs/networking-and-content-delivery/secure-your-lambda-function-urls-using-amazon-cloudfront-origin-access-control/) 2. Experiment for the optimal memory size and timeout for the Lambda handler to achieve better cost efficiency 3. Use provisioned throughput to further avoid Lambda cold starts 4. Support multiple API keys by updating `api.auth.api_key_auth` logic 5. Support non-text/image content, such as `DocumentContent` or `VideoContent` which are [well supported by Amazon Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlock.html).",
        "Special thanks to the contributors of the following two projects. As without their efforts, this cost effective gateway won't even exist:",
        "- [aws-samples/bedrock-access-gateway](https://github.com/aws-samples/bedrock-access-gateway) - [awslabs/aws-lambda-web-adapter](https://github.com/awslabs/aws-lambda-web-adapter)"
      ]
    },
    {
      "id": "article:get-phished-by-a-public-aws-systems-manager-automation-document-2die",
      "source_type": "article",
      "title": "Get Phished by a Public AWS Systems Manager Automation Document",
      "url": "https://gabrielkoo.com/blog/get-phished-by-a-public-aws-systems-manager-automation-document-2die/",
      "canonical_url": "https://dev.to/aws-builders/get-phished-by-a-public-aws-systems-manager-automation-document-2die",
      "published_at": "2024-12-23",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "security",
        "phishing"
      ],
      "description": "[Update 2025-01-09] Thanks Wiz.io for featuring this as a new phishing technique! ...",
      "content": "[Update 2025-01-09] Thanks Wiz.io for featuring this as a new phishing technique!\n<https://threats.wiz.io/all-techniques/ssm-document-phishing>\n\n---\n\nYou've probably heard the buzz about AWS unveiling the new Nova models at re:Invent 2024. Among them, the Nova Premier model stands out as the crown jewel—even though it’s still in training.\n\nCuriosity piqued, you start searching online and stumble upon a post from someone sharing a **link to the AWS Console**. The link appears to be a secret but public AWS Systems Manager document, purportedly used by internal staff to grant access to selected alpha testers for the Nova Premier model.\n\nThe URL looks legitimate, as it's point to a **real AWS Console URL**, with the **document ARN pre-filled**:\n\n```\nhttps://us-east-1.console.aws.amazon.com\n  /systems-manager\n  /automation\n  /execute\n  /arn%253Aaws%253Assm%253Aus-east-1%253A123456789012%253Adocument%252FAWSBedrock-SetupNovaPremier\n    ?region=us-east-1\n```\n\nYou notice the automation runbook is named `AWSBedrock-SetupNovaPremier`, which seems **credible** since many Automation documents start with `AWS-`.\n\n![The AWS SSM Document looks really normal](/assets/img/593a9fc88112.png)\n\nWithout thinking twice and because of your desire for the unreleased alpha Nova Premier model, you clicked the orange `Execute` button - and you will soon regret it.\n\n## Guess What You Are Phished\n\nImagine you’re an AWS user excited about the new Nova Premier model. You click on what you believe to be a legitimate link to gain early access. Unbeknownst to you, this link executes a **malicious SSM document** that could:\n\n- Grants unauthorized access to your AWS resources.\n- Exfiltrates sensitive data.\n- Deploys malware within your AWS environment.\n\nBy the time you realize something is wrong, the damage is already done.\n\nI successfully created a proof of concept that allows a victim AWS account to execute a public AWS SSM Automation Document created by the attacker account, that creates an IAM user:\n\n![Congratulations you are phished!](/assets/img/3e18367e8f00.png)\n\n## How Did the Attack Happen?\n\nThe attack leverages the trust users place in AWS Console links and Systems Manager documents. By crafting a URL that mimics a legitimate AWS-owned AWS Systems Manager Automation Document, attackers can trick users into executing malicious code. In this case, the link points to an SSM document that **appears official** but is actually designed to compromise the user's AWS environment.\n\n## What could have avoided the attack?\n\n1. **Awareness is Key**: Phishing can occur even with valid AWS console links. Just because a link appears to be from the AWS Console doesn’t mean it’s safe. This technique can also be applied to EC2 AMIs or Lambda Layers as both resource could be shared publicly.\n\n2. **[SSM Specific]** Check the Owner: Always verify the owner of an SSM Document before using it. Ensure it’s either \"Amazon\" or your own AWS account ID.\n\n3. **[For AWS] Enhanced Warnings**: AWS Console should display a prominent warning when a user attempts to execute an Automation document that is not owned by AWS or the underlying AWS account.\n\n4. **[For AWS] Prefix Blocking**: Consider blocking creation of documents that start with the `AWS` prefix - on top of the fact that documents with the `AWS-` prefix are already blocked according to [AWS documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-ssm-sharing.html#:~:text=In%20Systems%20Manager,all%20to%20use.)\n\n5. Refine permissions to execute SSM Automation Documents, or even avoid granting such permission in the first place.\n\nThat's it!\n\nHope this article does remind you to triple check any AWS Console URLs before you make any action on it!\n\n**Disclaimer**: Nova Premier has not yet released at the point of writing, and this article does not encourage any phishing/illegal activity. The PoC was created for illustration purpose only.",
      "excerpts": [
        "[Update 2025-01-09] Thanks Wiz.io for featuring this as a new phishing technique!",
        "You've probably heard the buzz about AWS unveiling the new Nova models at re:Invent 2024. Among them, the Nova Premier model stands out as the crown jewel—even though it’s still in training.",
        "Curiosity piqued, you start searching online and stumble upon a post from someone sharing a **link to the AWS Console**. The link appears to be a secret but public AWS Systems Manager document, purportedly used by internal staff to grant access to selected alpha testers for the Nova Premier model.",
        "The URL looks legitimate, as it's point to a **real AWS Console URL**, with the **document ARN pre-filled**:",
        "You notice the automation runbook is named `AWSBedrock-SetupNovaPremier`, which seems **credible** since many Automation documents start with `AWS-`.",
        "![The AWS SSM Document looks really normal](/assets/img/593a9fc88112.png)",
        "Without thinking twice and because of your desire for the unreleased alpha Nova Premier model, you clicked the orange `Execute` button - and you will soon regret it.",
        "Imagine you’re an AWS user excited about the new Nova Premier model. You click on what you believe to be a legitimate link to gain early access. Unbeknownst to you, this link executes a **malicious SSM document** that could:",
        "- Grants unauthorized access to your AWS resources. - Exfiltrates sensitive data. - Deploys malware within your AWS environment.",
        "By the time you realize something is wrong, the damage is already done.",
        "I successfully created a proof of concept that allows a victim AWS account to execute a public AWS SSM Automation Document created by the attacker account, that creates an IAM user:",
        "![Congratulations you are phished!](/assets/img/3e18367e8f00.png)",
        "The attack leverages the trust users place in AWS Console links and Systems Manager documents. By crafting a URL that mimics a legitimate AWS-owned AWS Systems Manager Automation Document, attackers can trick users into executing malicious code. In this case, the link points to an SSM document that **appears official** but is actually designed to compromise the user's AWS environment.",
        "What could have avoided the attack?",
        "1. **Awareness is Key**: Phishing can occur even with valid AWS console links. Just because a link appears to be from the AWS Console doesn’t mean it’s safe. This technique can also be applied to EC2 AMIs or Lambda Layers as both resource could be shared publicly.",
        "2. **[SSM Specific]** Check the Owner: Always verify the owner of an SSM Document before using it. Ensure it’s either \"Amazon\" or your own AWS account ID.",
        "3. **[For AWS] Enhanced Warnings**: AWS Console should display a prominent warning when a user attempts to execute an Automation document that is not owned by AWS or the underlying AWS account.",
        "4. **[For AWS] Prefix Blocking**: Consider blocking creation of documents that start with the `AWS` prefix - on top of the fact that documents with the `AWS-` prefix are already blocked according to [AWS documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-ssm-sharing.html#:~:text=In%20Systems%20Manager,all%20to%20use.)",
        "5. Refine permissions to execute SSM Automation Documents, or even avoid granting such permission in the first place.",
        "Hope this article does remind you to triple check any AWS Console URLs before you make any action on it!",
        "**Disclaimer**: Nova Premier has not yet released at the point of writing, and this article does not encourage any phishing/illegal activity. The PoC was created for illustration purpose only."
      ]
    },
    {
      "id": "article:open-unclickable-instagram-post-description-links-with-aws-lambda-and-ios-shortcuts-38h4",
      "source_type": "article",
      "title": "Open Unclickable Instagram Post Description Links with AWS Lambda and iOS Shortcuts",
      "url": "https://gabrielkoo.com/blog/open-unclickable-instagram-post-description-links-with-aws-lambda-and-ios-shortcuts-38h4/",
      "canonical_url": "https://dev.to/aws-builders/open-unclickable-instagram-post-description-links-with-aws-lambda-and-ios-shortcuts-38h4",
      "published_at": "2024-11-03",
      "last_verified_at": "2026-08-23",
      "tags": [
        "instagram",
        "serverless",
        "ios",
        "aws"
      ],
      "description": "Extracting Links from Instagram Posts Made Easy with AWS Lambda Have you ever tried to...",
      "content": "## Extracting Links from Instagram Posts Made Easy with AWS Lambda\n\nHave you ever tried to **just copy a link** from an **Instagram post caption**, only to realize Instagram doesn't allow clickable links in the caption? For most users, this limitation is more than just annoying, especially when you want to quickly open a link someone shared in a post, while don't want to get **distracted** too much.\n\nInstagram’s design forces users to go through several steps just to extract a link — if it's even possible at all. But don’t worry! I’m going to show you how to simplify this process using **AWS Lambda** and **iOS Shortcuts**. Best of all, you can set this up yourself with just a few steps, and it won’t cost you much, thanks to the efficiency and low cost of **AWS’s Serverless services**.\n\n### The Problem with Instagram Links\n\nInstagram doesn’t allow clickable links in post captions, which means you can’t simply click and go. People online mentioned this might be due to avoidance of **spam links or phishers**, which is legit, but again, annoying.\n\n### \"Open Link in Bio\"\n\nOne of the most common ways is to add a link in the bio, and then add a caption like \"Link in bio\". \n\nHowever, this is not ideal as it requires the user to click on the profile, and then click on the link in the bio - that's significant distraction already. Users could have been enjoying other contents in the feed, but now they are distracted by the \"Link in bio\" caption as they are forced to click on the profile to find the link. Also, this must be done by the content creator - as a user, this is helpless as you cannot guarantee every Instagram content creator follows this strictly.\n\nInstead, users are forced to find a way to extract and open the link.\n\n### Straightforward but Hard Way\n\n![Memorize a long link from an Instagram post description - because the content creator did care about your user experience](/assets/img/baa0001a51ab.png)\n\nFirst way, which I would not recommend - is to memorize the link into your brain, then type it manually in the browser. \n\nYou might have to go back to the Instagram App a few times if you are not confident with your short term memory.\n\n### The other Usual Painful Process:\n\nFor most users, extracting a link looks something like this:\n\n1. **Copy the post link** (from the post’s share options).\n2. **Paste the link in a browser** (Instagram opens in a browser).\n3. **Drag and highlight the link** (this can be tricky because you need to avoid missing any characters).\n4. **Copy the link** (if you managed to highlight it correctly).\n5. **Paste the copied link into a new browser tab** (finally!).\n\nThis whole process can be very frustrating and time-consuming, especially if you do it often.\n\n![Step by step way to extract a link in an Instagram post description](/assets/img/3334a5d741ec.gif)\n\n### The Live Text Alternative (for iOS Users)\n\nIf you’re using **iOS 15 or later**, you might try to use the **Live Text** feature. This allows you to take a screenshot of the Instagram post and then copy the text (including the link) from the image. However, even this method is far from perfect:\n\n- It is not very reliable.\n- You still need to manually select the link from the image, which can easily result in errors.\n\n![Using Live Text to extract a link in an Instagram post description](/assets/img/7e174f33a07a.gif)\n\n### The Fast and Easy Way: Use an iOS Shortcut!\n\nTo simplify things, I’ve developed a more efficient method using an **iOS Shortcut** combined with an **AWS Lambda function**. This approach allows you to extract links from Instagram posts in just a few seconds, with minimal effort—and it’s highly reliable.\n\n![Using Shortcuts with a AWS SAM backend to extract a link in an Instagram post description](/assets/img/a5aa91e372b4.gif)\n\nWhat is does under the hood:\n1. Accepts the shared post URL from Instagram App\n2. Makes a POST request to the AWS API Gateway endpoint to retrieve the list of extracted Instagram post links ([Python Script Here](https://github.com/gabrielkoo/insta-post-link-extractor/blob/main/src/handler.py))\n3. Let's the user pick one of the extracted (if any) links\n4. Open an extracted link in browser\n\nNow, you no longer have to either get distracted away from the original post, nor have to get anxious about highlighting the entire URL portion in the post.\n\n#### Key Benefits of This Shortcut:\n\n- **Speed**: Extracts links in about **5 seconds**.\n- **Reliability**: The shortcut directly accesses the raw HTML meta tag of the Instagram post, ensuring you get the correct link every time.\n- **Convenience**: No need to manually highlight or copy links anymore, or forced to navigate to the content creator's bio link.\n\nHere’s how the three methods compare:\n\n| Method              | Speed | Reliability | Description\n| ------------------- | ----- | ----------- | -----------\n| **Short Term Memory** | 30s | Medium (Depends) | Perfect if you love memorizing things and do more manual typing.\n| **Step by Step**     | 20s    | High        | Requires manually copying the post link, opening it in a browser, then dragging to highlight the link—tedious and error-prone.\n| **Live Text**        | 10s    | Low         | Uses iOS Live Text to copy the link from a screenshot—unreliable and still requires manual effort.\n| **This Shortcut**    | 5s     | High        | Automatically extracts the link via an AWS API Gateway API endpoint, with no need for manual copying or highlighting—fast and reliable.\n\n### How It Works\n\nThe shortcut works by sending the Instagram post URL to an **AWS Lambda** function, which extracts the link from the post and returns it. AWS Lambda is ideal for this task because it only runs when triggered, making it cost-effective as you only pay for what you use.\n\n### How to Deploy Your Own Instagram Link Extractor\n\nAlthough I’ve bundled the shortcut with my own deployed endpoint, I’ve set rate limits to prevent overuse. To avoid these limits and run it as much as you like, I recommend deploying your own version, which is easy and inexpensive.\n\nHere’s how you can deploy it yourself using **AWS CloudShell**.\n\n#### **Step 0: Get into AWS Console**\n\n#### **Step 1: Open AWS CloudShell**\n\nAWS CloudShell is a browser-based shell that allows you to interact with AWS services without needing to install anything locally. You can open CloudShell by visiting [AWS CloudShell](https://us-east-1.console.aws.amazon.com/cloudshell/home?region=us-east-1).\n\n#### **Step 2: Clone the GitHub Repository**\n\nIn CloudShell, run the following commands to clone the repository and navigate into the project directory:\n\n```bash\ngit clone https://github.com/gabrielkoo/insta-post-link-extractor.git\ncd insta-post-link-extractor\n```\n\n#### **Step 3: Build and Deploy the Lambda Function**\n\nNext, use the **AWS SAM CLI** to build and deploy the Lambda function. This will create the necessary resources in AWS, including the Lambda function and the API Gateway.\n\n```bash\n# Build the application\nsam build -x InstaPostLinkExtractorFunction\n\n# Deploy the application\nsam deploy --stack-name InstaPostLinkExtractor --guided\n\n# Follow the prompts to configure your deployment.\n```\n\nOnce the deployment is complete, AWS will output the **API Gateway URL** and **API Key** that you'll use in the shortcut.\n\n#### **Step 4: Retrieve API Details**\n\nAfter deployment, you will see the API Gateway URL and API Key in the deployment output. These are essential for configuring the iOS Shortcut.\n\n```bash\nCloudFormation outputs from deployed stack\n-----------------------------------------------------------------------------------------------------------------\nOutputs\n-----------------------------------------------------------------------------------------------------------------\nKey                 InstaPostLinkExtractorApiApiKeyConsoleUrl\nDescription         API Gateway API Key Console URL for InstaPostLinkExtractorApi\nValue               https://us-east-1.console.aws.amazon.com/apigateway/main/api-keys/[API_KEY_ID_HERE]?region=us-east-1\n\nKey                 InstaPostLinkExtractorApi\nDescription         API Gateway endpoint URL for Prod stage for InstaPostLinkExtractorApi\nValue               https://[APIGW_ID_HERE].execute-api.us-east-1.amazonaws.com/prod/insta-post-link-extractor/\n------------------------------------------------------------------------------------------------------------------\n```\n\n#### **Step 5: Test API with `curl`**\n\nTo test the API, use the following `curl` command. Replace the placeholder values with the **API Gateway URL** and **API Key** from the previous step.\n\n```bash\nexport ENDPOINT_URL='https://[APIGW_ID_HERE].execute-api.us-east-1.amazonaws.com/prod/insta-post-link-extractor/'\nexport API_KEY='[YOUR_API_KEY]'\n\n# Test the API\ncurl $ENDPOINT_URL \\\n    -X POST \\\n    -H \"x-api-key: $API_KEY\" \\\n    -H \"Accept: application/json\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"url\": \"https://www.instagram.com/p/DB5QbNzTQxY/\"}' | jq\n```\n\nIf everything is working, the API will return the extracted links in JSON format.\n\n```json\n{\n  \"links\": [\n    \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\",\n    \"https://www.youtube.com/watch?v=nsCIeklgp1M\"\n  ]\n}\n```\n\n#### **Step 6: Install the iOS Shortcut**\n\nNow that the API is working, you can set up the iOS Shortcut. Install the pre-bundled shortcut from this link: [Extract IG Post Links Shortcut](https://raw.githubusercontent.com/gabrielkoo/insta-post-link-extractor/refs/heads/main/Extract+IG+Post+Links.shortcut).\n\n\n![Install the Shortcut](/assets/img/dc2b27c23587.PNG)\n\n![Replace the values in the \"Get Contents of\" step](/assets/img/c4eed0363e8c.PNG)\n\nOnce installed, enter the **API_KEY** and **ENDPOINT_URL** values from your AWS deployment into the shortcut settings, namely the `Get contents of` step. Now, you can extract Instagram links with just a tap!\n\n### Why AWS Lambda?\n\nUsing **AWS Lambda** for this task makes it cheap and efficient. Lambda runs your code on demand, so you only pay for the computing time you actually use, which makes it a perfect solution for lightweight tasks like this one.\n\n### Conclusion\n\nWith this simple setup, you can eliminate the frustration of manually extracting links from Instagram posts. By deploying your own **Instagram Post Link Extractor**, you’ll be able to quickly pull links from posts with just a few taps, saving you time and hassle.\n\nTry it out for yourself by following the steps above, and start enjoying a faster, easier way of extracting Instagram links!\n\nLastly, feel free to modify it so that it can handle more cases - like extracting useful emails, phone numbers, or addresses from post descriptions!\n\nFor more details, check out the full project on GitHub: [gabrielkoo/insta-post-link-extractor](https://github.com/gabrielkoo/insta-post-link-extractor).\n\n---\n\n**Note**:\n- The iOS Shortcut I provided already includes my own deployed endpoint as well as the API key, but due to very restrictive rate limits, I recommend deploying your own version for unlimited use.\n- All images, except the screen recordings, have been generated by Amazon Titan Image Generator G1 V2.",
      "excerpts": [
        "Extracting Links from Instagram Posts Made Easy with AWS Lambda",
        "Have you ever tried to **just copy a link** from an **Instagram post caption**, only to realize Instagram doesn't allow clickable links in the caption? For most users, this limitation is more than just annoying, especially when you want to quickly open a link someone shared in a post, while don't want to get **distracted** too much.",
        "Instagram’s design forces users to go through several steps just to extract a link — if it's even possible at all. But don’t worry! I’m going to show you how to simplify this process using **AWS Lambda** and **iOS Shortcuts**. Best of all, you can set this up yourself with just a few steps, and it won’t cost you much, thanks to the efficiency and low cost of **AWS’s Serverless services**.",
        "The Problem with Instagram Links",
        "Instagram doesn’t allow clickable links in post captions, which means you can’t simply click and go. People online mentioned this might be due to avoidance of **spam links or phishers**, which is legit, but again, annoying.",
        "One of the most common ways is to add a link in the bio, and then add a caption like \"Link in bio\".",
        "However, this is not ideal as it requires the user to click on the profile, and then click on the link in the bio - that's significant distraction already. Users could have been enjoying other contents in the feed, but now they are distracted by the \"Link in bio\" caption as they are forced to click on the profile to find the link. Also, this must be done by the content creator - as a user, this is helpless as you cannot guarantee every Instagram content creator follows this strictly.",
        "Instead, users are forced to find a way to extract and open the link.",
        "![Memorize a long link from an Instagram post description - because the content creator did care about your user experience](/assets/img/baa0001a51ab.png)",
        "First way, which I would not recommend - is to memorize the link into your brain, then type it manually in the browser.",
        "You might have to go back to the Instagram App a few times if you are not confident with your short term memory.",
        "The other Usual Painful Process:",
        "For most users, extracting a link looks something like this:",
        "1. **Copy the post link** (from the post’s share options). 2. **Paste the link in a browser** (Instagram opens in a browser). 3. **Drag and highlight the link** (this can be tricky because you need to avoid missing any characters). 4. **Copy the link** (if you managed to highlight it correctly). 5. **Paste the copied link into a new browser tab** (finally!).",
        "This whole process can be very frustrating and time-consuming, especially if you do it often.",
        "![Step by step way to extract a link in an Instagram post description](/assets/img/3334a5d741ec.gif)",
        "The Live Text Alternative (for iOS Users)",
        "If you’re using **iOS 15 or later**, you might try to use the **Live Text** feature. This allows you to take a screenshot of the Instagram post and then copy the text (including the link) from the image. However, even this method is far from perfect:",
        "- It is not very reliable. - You still need to manually select the link from the image, which can easily result in errors.",
        "![Using Live Text to extract a link in an Instagram post description](/assets/img/7e174f33a07a.gif)",
        "The Fast and Easy Way: Use an iOS Shortcut!",
        "To simplify things, I’ve developed a more efficient method using an **iOS Shortcut** combined with an **AWS Lambda function**. This approach allows you to extract links from Instagram posts in just a few seconds, with minimal effort—and it’s highly reliable.",
        "![Using Shortcuts with a AWS SAM backend to extract a link in an Instagram post description](/assets/img/a5aa91e372b4.gif)",
        "What is does under the hood: 1. Accepts the shared post URL from Instagram App 2. Makes a POST request to the AWS API Gateway endpoint to retrieve the list of extracted Instagram post links ([Python Script Here](https://github.com/gabrielkoo/insta-post-link-extractor/blob/main/src/handler.py)) 3. Let's the user pick one of the extracted (if any) links 4. Open an extracted link in browser",
        "Now, you no longer have to either get distracted away from the original post, nor have to get anxious about highlighting the entire URL portion in the post.",
        "Key Benefits of This Shortcut:",
        "- **Speed**: Extracts links in about **5 seconds**. - **Reliability**: The shortcut directly accesses the raw HTML meta tag of the Instagram post, ensuring you get the correct link every time. - **Convenience**: No need to manually highlight or copy links anymore, or forced to navigate to the content creator's bio link.",
        "Here’s how the three methods compare:",
        "| Method | Speed | Reliability | Description | ------------------- | ----- | ----------- | ----------- | **Short Term Memory** | 30s | Medium (Depends) | Perfect if you love memorizing things and do more manual typing. | **Step by Step** | 20s | High | Requires manually copying the post link, opening it in a browser, then dragging to highlight the link—tedious and error-prone. | **Live Text** | 10s | Low | Uses iOS Live Text to copy the link from a screenshot—unreliable and still requires manual effort. | **This Shortcut** | 5s | High | Automatically extracts the link via an AWS API Gateway API endpoint, with no need for manual copying or highlighting—fast and reliable.",
        "The shortcut works by sending the Instagram post URL to an **AWS Lambda** function, which extracts the link from the post and returns it. AWS Lambda is ideal for this task because it only runs when triggered, making it cost-effective as you only pay for what you use.",
        "How to Deploy Your Own Instagram Link Extractor",
        "Although I’ve bundled the shortcut with my own deployed endpoint, I’ve set rate limits to prevent overuse. To avoid these limits and run it as much as you like, I recommend deploying your own version, which is easy and inexpensive.",
        "Here’s how you can deploy it yourself using **AWS CloudShell**.",
        "**Step 0: Get into AWS Console**",
        "**Step 1: Open AWS CloudShell**",
        "AWS CloudShell is a browser-based shell that allows you to interact with AWS services without needing to install anything locally. You can open CloudShell by visiting [AWS CloudShell](https://us-east-1.console.aws.amazon.com/cloudshell/home?region=us-east-1).",
        "**Step 2: Clone the GitHub Repository**",
        "In CloudShell, run the following commands to clone the repository and navigate into the project directory:",
        "**Step 3: Build and Deploy the Lambda Function**",
        "Next, use the **AWS SAM CLI** to build and deploy the Lambda function. This will create the necessary resources in AWS, including the Lambda function and the API Gateway.",
        "Deploy the application sam deploy --stack-name InstaPostLinkExtractor --guided",
        "Follow the prompts to configure your deployment. ```",
        "Once the deployment is complete, AWS will output the **API Gateway URL** and **API Key** that you'll use in the shortcut.",
        "**Step 4: Retrieve API Details**",
        "After deployment, you will see the API Gateway URL and API Key in the deployment output. These are essential for configuring the iOS Shortcut.",
        "Key InstaPostLinkExtractorApi Description API Gateway endpoint URL for Prod stage for InstaPostLinkExtractorApi Value https://[APIGW_ID_HERE].execute-api.us-east-1.amazonaws.com/prod/insta-post-link-extractor/ ------------------------------------------------------------------------------------------------------------------ ```",
        "**Step 5: Test API with `curl`**",
        "To test the API, use the following `curl` command. Replace the placeholder values with the **API Gateway URL** and **API Key** from the previous step.",
        "Test the API curl $ENDPOINT_URL \\ -X POST \\ -H \"x-api-key: $API_KEY\" \\ -H \"Accept: application/json\" \\ -H \"Content-Type: application/json\" \\ -d '{\"url\": \"https://www.instagram.com/p/DB5QbNzTQxY/\"}' | jq ```",
        "If everything is working, the API will return the extracted links in JSON format.",
        "**Step 6: Install the iOS Shortcut**",
        "Now that the API is working, you can set up the iOS Shortcut. Install the pre-bundled shortcut from this link: [Extract IG Post Links Shortcut](https://raw.githubusercontent.com/gabrielkoo/insta-post-link-extractor/refs/heads/main/Extract+IG+Post+Links.shortcut).",
        "![Install the Shortcut](/assets/img/dc2b27c23587.PNG)",
        "![Replace the values in the \"Get Contents of\" step](/assets/img/c4eed0363e8c.PNG)",
        "Once installed, enter the **API_KEY** and **ENDPOINT_URL** values from your AWS deployment into the shortcut settings, namely the `Get contents of` step. Now, you can extract Instagram links with just a tap!",
        "Using **AWS Lambda** for this task makes it cheap and efficient. Lambda runs your code on demand, so you only pay for the computing time you actually use, which makes it a perfect solution for lightweight tasks like this one.",
        "With this simple setup, you can eliminate the frustration of manually extracting links from Instagram posts. By deploying your own **Instagram Post Link Extractor**, you’ll be able to quickly pull links from posts with just a few taps, saving you time and hassle.",
        "Try it out for yourself by following the steps above, and start enjoying a faster, easier way of extracting Instagram links!",
        "Lastly, feel free to modify it so that it can handle more cases - like extracting useful emails, phone numbers, or addresses from post descriptions!",
        "For more details, check out the full project on GitHub: [gabrielkoo/insta-post-link-extractor](https://github.com/gabrielkoo/insta-post-link-extractor).",
        "**Note**: - The iOS Shortcut I provided already includes my own deployed endpoint as well as the API key, but due to very restrictive rate limits, I recommend deploying your own version for unlimited use. - All images, except the screen recordings, have been generated by Amazon Titan Image Generator G1 V2."
      ]
    },
    {
      "id": "article:i-bought-us-east-1com-a-look-at-security-dns-traffic-and-protecting-aws-users-15ng",
      "source_type": "article",
      "title": "I bought us-east-1.com: A Look at Security, DNS Traffic, and Protecting AWS Users",
      "url": "https://gabrielkoo.com/blog/i-bought-us-east-1com-a-look-at-security-dns-traffic-and-protecting-aws-users-15ng/",
      "canonical_url": "https://dev.to/aws-builders/i-bought-us-east-1com-a-look-at-security-dns-traffic-and-protecting-aws-users-15ng",
      "published_at": "2024-10-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "dns",
        "security"
      ],
      "description": "When people think about the term \"us-east-1\", they often think of AWS's very data center region that...",
      "content": "When people think about the term \"`us-east-1`\", they often think of AWS's very data center region that powers countless businesses worldwide. But what if someone registered the `us-east-1.com` domain? That’s exactly what I did, not to compete with Amazon but to enhance security in a world where cyber threats are more prevalent than ever. Let’s dive into why I bought `us-east-1.com`, the DNS traffic it receives, and how this domain serves as a safeguard for AWS users everywhere.\n\n## The Story Behind `us-east-1.com`\nIn December 2021, a thought struck me: Could it be that no one has registered `us-east-1.com` yet? Given the immense popularity of AWS's `us-east-1` region, I was surprised that such a domain was still available. So, I went ahead and acquired it.\n\nBut this wasn’t just about claiming an unused domain. By owning [`us-east-1.com`](https://us-east-1.com), I aimed to protect AWS users from malicious actors who might misuse it for phishing or other attacks. Imagine the potential for someone using a domain like this to create convincing but fake AWS log-in pages or phishing schemes. By owning it, I can ensure that doesn’t happen (further with my professionalism as an AWS Community Builder).\n\n## A Peek into the DNS Traffic\nOwning this domain has provided fascinating insights into DNS queries—many of which are likely unintentional, generated by AWS resources and misconfigured systems. Here are the top daily DNS queries made against `us-east-1.com`:\n\n![Image description](/assets/img/2ccec319d9a0.png)\n\n### `prod-backend-db.cc66xuedqt2t.us-east-1.com` - 23,420 queries/day\nThis entry is the most frequent DNS request, suggesting a significant number of backend systems inadvertently reach out to this domain. It's possible that development or testing environments are mistakenly set to `us-east-1.com` instead of AWS’s official DNS.\n\nActually if you are familiar with _that AWS database service_, you might get some clues on what it is.\n\n### `us-east-1.com` - 10,890 queries/day\nOf course - this is the top level root domain.\n\n### `loopback-streaming.us-east-1.com` - 8,140 queries/day\nThe term \"loopback\" suggests that this may be tied to internal testing or streaming setups that inadvertently use `us-east-1.com`.\n\n### ! Cisco Static File Reputation Host ! - 2,000 queries/day [Updated on 2024-11-23]\n\nFor `cloud-sa-589592150.us-east-1.com` I traced a bit from Google, it was meant to be `cloud-sa-589592150.us-east-1.elb.amazonaws.com`, as according to [Cisco's document here](https://www.cisco.com/c/en/us/support/docs/security/email-security-appliance/210534-Configuring-static-File-Reputation-host.html) it is meant to be used as part of a legacy version of their email security gateway.\n\nGreat! To the system admin who set this up, your organization's email security is now at risk since my domain could have let some malicious files enter your organization's email domain.\n\n### Cloud-Specific Service Entries\n\nDomains like `storagegateway.us-east-1.com` and `s3.us-east-1.com` likely originate from services configured to use `us-east-1.com`. This highlights how systems might inadvertently look to this domain for data, increasing the risk of data leakage if the domain were in malicious hands.\n\nMy quick guess is that the user originally wanted to use `vpce-randomhash-randomhash.storagegateway.us-east-1.vpce.amazonaws.com`, but the domain name was manually typed and missed the `.vpce.amazonaws.com` part instead.\n\n### Unexpected Services\n\nWe see other services like `smtp.mail.us-east-1.com` and `mobile.mail.us-east-1.com` with smaller query counts. This could indicate email configurations or mobile services that are erroneously pointed here.\n\n### Extra - Flood of Unexpected Emails from (Official?) AWS Test Environments\n\n![Image description](/assets/img/639c43f3f475.png)\n\n![Emails were sent to my `@us-east-1.com` mailbox from *Thu, Dec 21, 2023, 12:45 AM* to *Sat, Dec 23, 2023, 10:40 PM*, around 60,000 in total](/assets/img/18e27b7171a9.png)\n\nIn addition to the DNS traffic, in 2023 December I’ve also received thousands of emails sent to one of `us-east-1.com`'s email address, presumably from an internal AWS team using placeholder email accounts during testing. These emails, like the one shown here, often contain messages about \"Data Requests\" and come from addresses structured like `aws-supply-chain@us-east-1.gamma.app.ketchup.aws.dev`. \n\nFrom the [WHOIS information](https://whois.com/whois/aws.dev) of the sender email's top level domain (`aws.dev.`), it should be owned by AWS officially, so likely there was an internal team testing something, potentially related to a supply chain application or data request system within AWS. However, instead of using internal or sandbox domains, the test emails were mistakenly directed to `@us-east-1.com`, likely due to placeholder or misconfigured email settings.\n\nThese emails underscore the importance of using secure, controlled environments for internal testing—especially in a company as large as AWS. Even minor oversights in placeholder email addresses can lead to unintended consequences, such as sending potentially sensitive internal only information to external entities.\n\n**Security Lesson**: For cloud developers and architects, this is a good reminder to double-check your email configurations and ensure testing setups use proper sandbox or internal domains. Misconfigurations, even small ones, can lead to unintended data exposure.\nIn addition, do periodically check your email API logs for any abnormal/unexpected emails being sent - so that mis-firing of emails like this case could be avoided.\n\n## Why This Matters for Security\n\nIf someone else owned `us-east-1.com`, they could potentially:\n\n- Set up a fake login portal that mimics the AWS Console.\n- Capture sensitive DNS queries that could reveal system configurations or IP addresses.\n- Use it as a phishing link to trick users into providing credentials or accessing malware\n   - Imagine if the domain was used to host a fake S3 API endpoint and people using the placeholder domain did uploaded real documents into it...\n\nBy owning this domain, I prevent these risks, ensuring AWS users aren’t unknowingly sending requests to a potentially malicious server. And for anyone reading this—always verify URLs before clicking. Even a slight typo can lead you into a trap.\n\n## What This Means for AWS Users\n\nAWS has built a robust cloud ecosystem, but users are responsible for secure configurations. Here’s what AWS users can learn from this:\n\n### Check Your DNS Configurations:\n\nMake sure your resources point to official AWS endpoints. Misconfigured DNS entries can inadvertently send sensitive information to unintended locations.\n\n### Be Mindful of Typos:\n\nIt’s easy to accidentally enter `us-east-1.com` instead of the official AWS domain. Double-check addresses, especially when dealing with cloud resources.\n\n### Stay Vigilant Against Phishing Attacks:\n\nAlways verify links, especially when accessing services critical to your infrastructure. Bookmarking official AWS links is a good practice to prevent phishing attempts.\n\n## Leverage a DNS Firewall like AWS Route 53 Resolver DNS Firewall:\n\nTo avoid your resources hosted on AWS from misusing the wrong domain, consider using Route 53 Resolver DNS Firewall. This service allows you to filter and regulate outbound DNS queries, helping prevent data exfiltration and accidental requests to unintended domains. You can create rule groups to block requests to specific domains or IP ranges, ensuring your resources only communicate with trusted endpoints. This added layer of security can help mitigate risks associated with misconfigurations or typos in domain names - in case your resources connected to the wrong domain, and the wrong domain is owned by a typosquater unlike myself.\n\nRelated reading: \n\n<https://dev.to/aws-builders/phantom-dns-query-to-gcp-vm-metadata-service-in-my-aws-workload-revealed-by-route-53-resolver-3c75>\n\nAs for your personal usages, do consider using a DNS resolver with built-in protection like `1.1.1.2`, AdGuard, or if you are a hands on person, try building your own [Pi-Hole](https://pi-hole.net/) and use it with a block list.\n\n## The Future of `us-east-1.com`\n\nOwning `us-east-1.com` has given me insights into how resources are configured in various environments. While I monitor DNS requests, my primary goal is to ensure this domain remains out of the hands of bad actors. It serves as a reminder of the simple yet effective ways we can improve security by managing key assets—like domains.\n\nI am welcome for suggestions on how to help AWS users detect misconfigurations or use this as a case study for security awareness in the cloud space. Until then, `us-east-1.com` remains a safe, controlled domain, protecting AWS users from potential security threats.\n\n## Ending Words\n\nRegistering `us-east-1.com` was a simple yet effective step to secure AWS users worldwide. This domain acts as a shield against phishing, data leaks, and other risks, simply by preventing misuse. If you’re an AWS user or anyone working with cloud services, take this as a reminder to double-check your configurations, always be wary of URLs, and adopt a proactive approach to security.\n\n*Security Tip*: Please **DO BOOKMARK** the official AWS console links or type them manually to avoid phishing attempts. Here’s the correct link for `us-east-1`: [AWS Console us-east-1](https://us-east-1.console.aws.amazon.com/).\n\n## Anyone Took Domains of Other Regions?\n\nCheck this out: <https://aws-region-domains.github.io>\n\n## Special Thanks [2024-01-29]\n\nSince my article has been published in October 27, I am really thankful for those who reshared it as well as those discussing my story for raising the awareness of domain/DNS security:\n\n<https://x.com/clintgibler/status/1858614453971165216>\n\n<https://x.com/payloadartist/status/1884235731377479831>\n<https://x.com/shakedko/status/1888167926936514763>\n<https://www.domaintools.com/resources/podcasts/dns-gone-rogue-darpas-cyber-puzzle-lessons-in-security-innovation/>\n<https://www.youtube.com/watch?v=9FrseL52cZ0>\n\nP.S. I have only registered `us-east-1.com`, any other domain names resembling other AWS popular regions are not affiliated with myself.",
      "excerpts": [
        "When people think about the term \"`us-east-1`\", they often think of AWS's very data center region that powers countless businesses worldwide. But what if someone registered the `us-east-1.com` domain? That’s exactly what I did, not to compete with Amazon but to enhance security in a world where cyber threats are more prevalent than ever. Let’s dive into why I bought `us-east-1.com`, the DNS traffic it receives, and how this domain serves as a safeguard for AWS users everywhere.",
        "The Story Behind `us-east-1.com` In December 2021, a thought struck me: Could it be that no one has registered `us-east-1.com` yet? Given the immense popularity of AWS's `us-east-1` region, I was surprised that such a domain was still available. So, I went ahead and acquired it.",
        "But this wasn’t just about claiming an unused domain. By owning [`us-east-1.com`](https://us-east-1.com), I aimed to protect AWS users from malicious actors who might misuse it for phishing or other attacks. Imagine the potential for someone using a domain like this to create convincing but fake AWS log-in pages or phishing schemes. By owning it, I can ensure that doesn’t happen (further with my professionalism as an AWS Community Builder).",
        "A Peek into the DNS Traffic Owning this domain has provided fascinating insights into DNS queries—many of which are likely unintentional, generated by AWS resources and misconfigured systems. Here are the top daily DNS queries made against `us-east-1.com`:",
        "![Image description](/assets/img/2ccec319d9a0.png)",
        "`prod-backend-db.cc66xuedqt2t.us-east-1.com` - 23,420 queries/day This entry is the most frequent DNS request, suggesting a significant number of backend systems inadvertently reach out to this domain. It's possible that development or testing environments are mistakenly set to `us-east-1.com` instead of AWS’s official DNS.",
        "Actually if you are familiar with _that AWS database service_, you might get some clues on what it is.",
        "`us-east-1.com` - 10,890 queries/day Of course - this is the top level root domain.",
        "`loopback-streaming.us-east-1.com` - 8,140 queries/day The term \"loopback\" suggests that this may be tied to internal testing or streaming setups that inadvertently use `us-east-1.com`.",
        "! Cisco Static File Reputation Host ! - 2,000 queries/day [Updated on 2024-11-23]",
        "For `cloud-sa-589592150.us-east-1.com` I traced a bit from Google, it was meant to be `cloud-sa-589592150.us-east-1.elb.amazonaws.com`, as according to [Cisco's document here](https://www.cisco.com/c/en/us/support/docs/security/email-security-appliance/210534-Configuring-static-File-Reputation-host.html) it is meant to be used as part of a legacy version of their email security gateway.",
        "Great! To the system admin who set this up, your organization's email security is now at risk since my domain could have let some malicious files enter your organization's email domain.",
        "Cloud-Specific Service Entries",
        "Domains like `storagegateway.us-east-1.com` and `s3.us-east-1.com` likely originate from services configured to use `us-east-1.com`. This highlights how systems might inadvertently look to this domain for data, increasing the risk of data leakage if the domain were in malicious hands.",
        "My quick guess is that the user originally wanted to use `vpce-randomhash-randomhash.storagegateway.us-east-1.vpce.amazonaws.com`, but the domain name was manually typed and missed the `.vpce.amazonaws.com` part instead.",
        "We see other services like `smtp.mail.us-east-1.com` and `mobile.mail.us-east-1.com` with smaller query counts. This could indicate email configurations or mobile services that are erroneously pointed here.",
        "Extra - Flood of Unexpected Emails from (Official?) AWS Test Environments",
        "![Image description](/assets/img/639c43f3f475.png)",
        "![Emails were sent to my `@us-east-1.com` mailbox from *Thu, Dec 21, 2023, 12:45 AM* to *Sat, Dec 23, 2023, 10:40 PM*, around 60,000 in total](/assets/img/18e27b7171a9.png)",
        "In addition to the DNS traffic, in 2023 December I’ve also received thousands of emails sent to one of `us-east-1.com`'s email address, presumably from an internal AWS team using placeholder email accounts during testing. These emails, like the one shown here, often contain messages about \"Data Requests\" and come from addresses structured like `aws-supply-chain@us-east-1.gamma.app.ketchup.aws.dev`.",
        "From the [WHOIS information](https://whois.com/whois/aws.dev) of the sender email's top level domain (`aws.dev.`), it should be owned by AWS officially, so likely there was an internal team testing something, potentially related to a supply chain application or data request system within AWS. However, instead of using internal or sandbox domains, the test emails were mistakenly directed to `@us-east-1.com`, likely due to placeholder or misconfigured email settings.",
        "These emails underscore the importance of using secure, controlled environments for internal testing—especially in a company as large as AWS. Even minor oversights in placeholder email addresses can lead to unintended consequences, such as sending potentially sensitive internal only information to external entities.",
        "**Security Lesson**: For cloud developers and architects, this is a good reminder to double-check your email configurations and ensure testing setups use proper sandbox or internal domains. Misconfigurations, even small ones, can lead to unintended data exposure. In addition, do periodically check your email API logs for any abnormal/unexpected emails being sent - so that mis-firing of emails like this case could be avoided.",
        "If someone else owned `us-east-1.com`, they could potentially:",
        "- Set up a fake login portal that mimics the AWS Console. - Capture sensitive DNS queries that could reveal system configurations or IP addresses. - Use it as a phishing link to trick users into providing credentials or accessing malware - Imagine if the domain was used to host a fake S3 API endpoint and people using the placeholder domain did uploaded real documents into it...",
        "By owning this domain, I prevent these risks, ensuring AWS users aren’t unknowingly sending requests to a potentially malicious server. And for anyone reading this—always verify URLs before clicking. Even a slight typo can lead you into a trap.",
        "AWS has built a robust cloud ecosystem, but users are responsible for secure configurations. Here’s what AWS users can learn from this:",
        "Check Your DNS Configurations:",
        "Make sure your resources point to official AWS endpoints. Misconfigured DNS entries can inadvertently send sensitive information to unintended locations.",
        "It’s easy to accidentally enter `us-east-1.com` instead of the official AWS domain. Double-check addresses, especially when dealing with cloud resources.",
        "Stay Vigilant Against Phishing Attacks:",
        "Always verify links, especially when accessing services critical to your infrastructure. Bookmarking official AWS links is a good practice to prevent phishing attempts.",
        "Leverage a DNS Firewall like AWS Route 53 Resolver DNS Firewall:",
        "To avoid your resources hosted on AWS from misusing the wrong domain, consider using Route 53 Resolver DNS Firewall. This service allows you to filter and regulate outbound DNS queries, helping prevent data exfiltration and accidental requests to unintended domains. You can create rule groups to block requests to specific domains or IP ranges, ensuring your resources only communicate with trusted endpoints. This added layer of security can help mitigate risks associated with misconfigurations or typos in domain names - in case your resources connected to the wrong domain, and the wrong domain is owned by a typosquater unlike myself.",
        "As for your personal usages, do consider using a DNS resolver with built-in protection like `1.1.1.2`, AdGuard, or if you are a hands on person, try building your own [Pi-Hole](https://pi-hole.net/) and use it with a block list.",
        "Owning `us-east-1.com` has given me insights into how resources are configured in various environments. While I monitor DNS requests, my primary goal is to ensure this domain remains out of the hands of bad actors. It serves as a reminder of the simple yet effective ways we can improve security by managing key assets—like domains.",
        "I am welcome for suggestions on how to help AWS users detect misconfigurations or use this as a case study for security awareness in the cloud space. Until then, `us-east-1.com` remains a safe, controlled domain, protecting AWS users from potential security threats.",
        "Registering `us-east-1.com` was a simple yet effective step to secure AWS users worldwide. This domain acts as a shield against phishing, data leaks, and other risks, simply by preventing misuse. If you’re an AWS user or anyone working with cloud services, take this as a reminder to double-check your configurations, always be wary of URLs, and adopt a proactive approach to security.",
        "*Security Tip*: Please **DO BOOKMARK** the official AWS console links or type them manually to avoid phishing attempts. Here’s the correct link for `us-east-1`: [AWS Console us-east-1](https://us-east-1.console.aws.amazon.com/).",
        "Anyone Took Domains of Other Regions?",
        "Since my article has been published in October 27, I am really thankful for those who reshared it as well as those discussing my story for raising the awareness of domain/DNS security:",
        "P.S. I have only registered `us-east-1.com`, any other domain names resembling other AWS popular regions are not affiliated with myself."
      ]
    },
    {
      "id": "article:deep-linking-aws-console-with-all-your-aws-iam-identity-center-roles-148c",
      "source_type": "article",
      "title": "Deep Linking AWS Console with all your AWS IAM Identity Center Roles",
      "url": "https://gabrielkoo.com/blog/deep-linking-aws-console-with-all-your-aws-iam-identity-center-roles-148c/",
      "canonical_url": "https://dev.to/aws-builders/deep-linking-aws-console-with-all-your-aws-iam-identity-center-roles-148c",
      "published_at": "2024-08-17",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "iam",
        "security",
        "identity"
      ],
      "description": "Simplifying AWS IAM Identity Center Role Management As an AWS Community Builder, I'm...",
      "content": "## Simplifying AWS IAM Identity Center Role Management\n\nAs an AWS Community Builder, I'm always looking for ways to streamline cloud management tasks. Today, I'm excited to share a tool I've developed to make AWS IAM Identity Center (formerly AWS SSO) role management easier and more efficient: the AWS IAM Identity Center Access Role Portal:\n\n<https://iamidentitycenterroles.us-east-1.com/>\n\n(I am the owner of the domain `us-east-1.com`, and it's not directly affiliated with AWS.)\n\n## What is AWS IAM Identity Center?\n\nAs per the [official docs](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html),\n\n> AWS IAM Identity Center is the recommended AWS service for managing human user access to AWS resources. It is a single place where you can assign your workforce users, also known as workforce identities, consistent access to multiple AWS accounts and applications.\n\nIt's easier to manage than using traditional IAM Users for accessing AWS Console/CLI, where you have to configure the users/role manually in every AWS account that you own. With AWS IAM Identity Center, you can configure/delegate the same role(s) into multiple Organization AWS Accounts in one go.\n\n## The Challenge\n\nIf you do need to access multiple AWS accounts via different IAM roles in your day to day job, switching to different roles across multiple AWS accounts can be a troublesome task, especially for organizations with numerous teams and projects. \n\nOne traditional way would be to use the [IAM \"Switch Role\" feature](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-console.html). It requires you to create an AWS IAM user in one AWS account, then create the IAM roles in each of the \"target\" AWS accounts where you wish to access manually one by one.\n\nThankfully, with AWS IAM Identity Center, you can access the roles from different AWS accounts within the same AWS Organization in a centralized way. Users can switch between different AWS Account IAM roles effectively.\n\nFurther if you work for multiple organizations (say you are a consultant or your company has several subsidiaries), you may need to access different AWS Organizations within a single day. In this case, you have to navigate between multiple AWS Identity Center access portals in order to get access to various AWS resources.\n\n## Thanks to AWS, there is a new Shortcut feature\n\nThankfully, in 2024 April, AWS also released a [new shortcut feature](https://aws.amazon.com/about-aws/whats-new/2024/04/aws-iam-identity-center-shortcut-links-aws-access-portal/)) within AWS IAM Identity Center:\n\n![New \"Create Shortcut\" feature of AWS IAM Identity Center](/assets/img/b9c686171f99.png)\n\nWith the simple form, you can generate a deep URL link that once accessed, authenticates you to the AWS IAM Identity Center organization, assumes the IAM access role, and optionally redirects you back to a page within AWS Console.\n\nFor example, you may look at this sample deep linking URL\n\n```\nhttps://gabrielkoo.awsapps.com/start/#/console?\n  account_id=123456789012&\n  role_name=DeepracerRole&\n  destination=https%3A%2F%2Fus-east-1.console.aws.amazon.com%2Fdeepracer%2Fhome%3Fregion%3Dus-east-1%23models\n```\n\nWhich when URL-decoded, it becomes:\n\n```\nhttps://gabrielkoo.awsapps.com/start/#/console?\n  account_id=123456789012&\n  role_name=DeepracerRole&\n  destination=https://us-east-1.console.aws.amazon.com/deepracer/home?region=us-east-1#models\n```\n\nGeneralizing it, it becomes:\n\n```\nhttps://<IDENTITY_CENTER_ALIAS>.awsapps.com/start/#/console?\n  account_id=<AWS_ACCOUNT_ID>&\n  role_name=<SSO_ACCESS_ROLE>&\n  destination=<AWS_CONSOLE_CALLBACK_URL>\n```\n\n## The Solution\n\nBased on the problem above, I created the AWS IAM Identity Center Access Role Portal - it is a lightweight, client-side web application that provides a unified interface for managing AWS IAM Identity Center's access roles - and most importantly it supports multiple AWS Organizations. \n\nLet's take a closer look at its features and benefits.\n\n### Easy Role Management\n\nThe portal presents all your roles in a clear, tabular format:\n\n\n![Role List](/assets/img/b489723673a5.png)\n\nFrom this single view, you can:\n- See all roles across different Identity Center aliases and AWS accounts\n- Quickly access the AWS console for any role\n- Edit role details\n- Clone a role record to a new record\n- Delete roles when they're no longer needed\n- Export and import as URLs: this is particular useful if you want to share a list of deep links to your teammates!\n\n### Streamlined Role Creation\n\nAdding a new role is as simple as clicking a button and filling out the form:\n\n![New Record Creation](/assets/img/3ab86015194b.png)\n\nThe form even allows you to paste a shortcut link (with the new feature) generated from AWS IAM Identity Center to pre-fill values, saving you time and reducing the chance of errors.\n\n### So when does it help?\n\nWhen integrated with other AWS Console's pages that offers deep linking, you would just can't imagine all the possibilities that it might unblock.\n\nFor example, you have an AWS System Manager managed EC2 instance that serves as the company VPN.\n\nYour team grows and the VPN instance often suffers from memory leak - and you know the best way is to reboot it. You have been running a Systems Manager runbook `AWS-RestartEC2Instance` for a while. One day you reserves an urgent request to fix the VPN as one of your business user is doing an external demo with an internal site that requires VPN connectivity.\n\n> Every second for the \"reboot fix\" matters.\n\nInstead of these long steps...\n1. Log into AWS IAM Identity Center\n2. Pick the right AWS Account\n3. Pick the Access Role\n4. Get into AWS Console\n5. Navigate to EC2 Console\n6. Pick your instance\n7. Click \"Restart\"\n\nYou can just\n1. Click a new deep linked URL\n2. Log into AWS IAM Identity Center\n3. Execute a Systems Manager automation document with all parameters pre-filled:\n   ```\nhttps://us-east-1.console.aws.amazon.com/systems-manager/automation/execute/AWS-RestartEC2Instance?\n  region=us-east-1#InstanceId=<INSTANCE_ID>&AutomationAssumeRole=<ARN_OF_IAM_ROLE>\n   ```\n\nIt's much more handy than the initial 7-step way!\n\n## Security First\n\nAs an AWS Community Builder, I understand the critical importance of security, especially with this entry point of AWS Console. The IAM Identity Center Access Role Portal has been designed with security as a top priority:\n\n1. **Client-Side Only:** The entire application runs in the browser. There's no server-side component, which means there's no additional infrastructure to secure and maintain.\n\n2. **No Backend Data Storage:** The portal doesn't store any sensitive information. It's a pure interface tool that interacts directly with your AWS environment. Data is stored in your browser's local storage only.\n\n3. **Leverages Existing AWS Security:** By using AWS IAM Identity Center, the tool inherits AWS's robust security measures, such as multi-factor authentication and fine-grained permissions if you use AWS Identity Center's managed user directory.\n\n## Easy Deployment\n\nBecause the Access Role Portal is a static web application, deployment is straightforward. You can just use my version as-is, or download the static files from my GitHub repository.\n\n## Onboard your colleagues much faster\n\nWith my tools' import & export functions, AWS account admins can prepare a list deep linked URLs of different accounts and console pages, export it and share to your organization's new joiners. Now they can get access to various team resources much easier, which just copy pasting:\n\n![Image description](/assets/img/fb48bd8872a2.png)\n\nThen, you no longer need to team your new colleague every time on \"how to access the CloudWatch Log group for that Lambda function?\" - You can just share them the exported URL and the will have the book marks imported in their portal already.\n\nTry [this sample exported link](https://iamidentitycenterroles.us-east-1.com#data=W3siaWQiOiIxNzIzMzUzNjA4Nzk3IiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImdhYnJpZWxrb28iLCJhY2NvdW50SWQiOiIxMzU3OTI0NjgxMDEiLCJyb2xlTmFtZSI6IkRlZXByYWNlciIsImRpc3BsYXlOYW1lIjoiRGVlcHJhY2VyQWNjZXNzIiwicmVkaXJlY3RVcmkiOiJodHRwczovL3VzLWVhc3QtMS5jb25zb2xlLmF3cy5hbWF6b24uY29tL2RlZXByYWNlci9ob21lP3JlZ2lvbj11cy1lYXN0LTEjbW9kZWxzIn0seyJpZCI6IjE3MjMzNTM5MjUxOTEiLCJpZGVudGl0eUNlbnRlckFsaWFzIjoiY29tcGFueWIiLCJhY2NvdW50SWQiOiIyMzQ1Njc4OTAxMjMiLCJyb2xlTmFtZSI6Ik5ldHdvcmtFbmdpbmVlciIsImRpc3BsYXlOYW1lIjoiTmV0d29ya0VuZ2luZWVyLUIiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtZWFzdC0xLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vdnBjL2hvbWU/cmVnaW9uPXVzLWVhc3QtMSJ9LHsiaWQiOiIxNzIzMzUzOTM2MDQzIiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImNvbXBhbnljIiwiYWNjb3VudElkIjoiMzQ1Njc4OTAxMjM0Iiwicm9sZU5hbWUiOiJTZWN1cml0eUFuYWx5c3QiLCJkaXNwbGF5TmFtZSI6IlNlY3VyaXR5LUMiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtd2VzdC0xLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vZ3VhcmRkdXR5L2hvbWU/cmVnaW9uPXVzLXdlc3QtMSJ9LHsiaWQiOiIxNzIzMzUzOTQ2MTI3IiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImNvbXBhbnlkIiwiYWNjb3VudElkIjoiNDU2Nzg5MDEyMzQ1Iiwicm9sZU5hbWUiOiJDbG91ZEFyY2hpdGVjdCIsImRpc3BsYXlOYW1lIjoiQXJjaGl0ZWN0LUQiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtZWFzdC0yLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vY2xvdWRmb3JtYXRpb24vaG9tZT9yZWdpb249dXMtZWFzdC0yIn0seyJpZCI6IjE3MjMzNTM5NTU0ODIiLCJpZGVudGl0eUNlbnRlckFsaWFzIjoiY29tcGFueWUiLCJhY2NvdW50SWQiOiI1Njc4OTAxMjM0NTYiLCJyb2xlTmFtZSI6IkRldk9wc0VuZ2luZWVyIiwiZGlzcGxheU5hbWUiOiJEZXZPcHMtRSIsInJlZGlyZWN0VXJpIjoiaHR0cHM6Ly9hcC1zb3V0aGVhc3QtMS5jb25zb2xlLmF3cy5hbWF6b24uY29tL2Vjcy9ob21lP3JlZ2lvbj1hcC1zb3V0aGVhc3QtMSJ9LHsiaWQiOiIxNzIzMzUzOTEwOTM3IiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImNvbXBhbnlhIiwiYWNjb3VudElkIjoiMTIzNDU2Nzg5MDEyIiwicm9sZU5hbWUiOiJEYXRhYmFzZUFkbWluIiwiZGlzcGxheU5hbWUiOiJEQkFkbWluLUEiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtd2VzdC0yLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vcmRzL2hvbWU/cmVnaW9uPXVzLXdlc3QtMiJ9XQ==) in an incognito tab!\n\n## Conclusion\n\nThe AWS IAM Identity Center Access Role Portal demonstrates how simple tools can significantly improve our cloud management workflows. By providing a clear, unified interface for role management, it saves time, reduces errors, and enhances security.\n\nAs an AWS Community Builder, I'm committed to sharing knowledge and tools that help the community. I hope this portal will be useful for other AWS administrators dealing with multi-account environments.\n\nThe code for this project is open-source and available on GitHub <https://github.com/gabrielkoo/aws-iam-identity-center-shortcut-portal>. I welcome contributions and feedback from the community to make this tool even better.\n\nRemember, effective IAM management is crucial for maintaining a secure and efficient AWS environment. Tools like this can help, but they should always be used in conjunction with AWS best practices and regular security audits.\n\nHappy cloud computing!",
      "excerpts": [
        "Simplifying AWS IAM Identity Center Role Management",
        "As an AWS Community Builder, I'm always looking for ways to streamline cloud management tasks. Today, I'm excited to share a tool I've developed to make AWS IAM Identity Center (formerly AWS SSO) role management easier and more efficient: the AWS IAM Identity Center Access Role Portal:",
        "(I am the owner of the domain `us-east-1.com`, and it's not directly affiliated with AWS.)",
        "What is AWS IAM Identity Center?",
        "As per the [official docs](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html),",
        "> AWS IAM Identity Center is the recommended AWS service for managing human user access to AWS resources. It is a single place where you can assign your workforce users, also known as workforce identities, consistent access to multiple AWS accounts and applications.",
        "It's easier to manage than using traditional IAM Users for accessing AWS Console/CLI, where you have to configure the users/role manually in every AWS account that you own. With AWS IAM Identity Center, you can configure/delegate the same role(s) into multiple Organization AWS Accounts in one go.",
        "If you do need to access multiple AWS accounts via different IAM roles in your day to day job, switching to different roles across multiple AWS accounts can be a troublesome task, especially for organizations with numerous teams and projects.",
        "One traditional way would be to use the [IAM \"Switch Role\" feature](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-console.html). It requires you to create an AWS IAM user in one AWS account, then create the IAM roles in each of the \"target\" AWS accounts where you wish to access manually one by one.",
        "Thankfully, with AWS IAM Identity Center, you can access the roles from different AWS accounts within the same AWS Organization in a centralized way. Users can switch between different AWS Account IAM roles effectively.",
        "Further if you work for multiple organizations (say you are a consultant or your company has several subsidiaries), you may need to access different AWS Organizations within a single day. In this case, you have to navigate between multiple AWS Identity Center access portals in order to get access to various AWS resources.",
        "Thanks to AWS, there is a new Shortcut feature",
        "Thankfully, in 2024 April, AWS also released a [new shortcut feature](https://aws.amazon.com/about-aws/whats-new/2024/04/aws-iam-identity-center-shortcut-links-aws-access-portal/)) within AWS IAM Identity Center:",
        "![New \"Create Shortcut\" feature of AWS IAM Identity Center](/assets/img/b9c686171f99.png)",
        "With the simple form, you can generate a deep URL link that once accessed, authenticates you to the AWS IAM Identity Center organization, assumes the IAM access role, and optionally redirects you back to a page within AWS Console.",
        "For example, you may look at this sample deep linking URL",
        "Which when URL-decoded, it becomes:",
        "Based on the problem above, I created the AWS IAM Identity Center Access Role Portal - it is a lightweight, client-side web application that provides a unified interface for managing AWS IAM Identity Center's access roles - and most importantly it supports multiple AWS Organizations.",
        "Let's take a closer look at its features and benefits.",
        "The portal presents all your roles in a clear, tabular format:",
        "![Role List](/assets/img/b489723673a5.png)",
        "From this single view, you can: - See all roles across different Identity Center aliases and AWS accounts - Quickly access the AWS console for any role - Edit role details - Clone a role record to a new record - Delete roles when they're no longer needed - Export and import as URLs: this is particular useful if you want to share a list of deep links to your teammates!",
        "Adding a new role is as simple as clicking a button and filling out the form:",
        "![New Record Creation](/assets/img/3ab86015194b.png)",
        "The form even allows you to paste a shortcut link (with the new feature) generated from AWS IAM Identity Center to pre-fill values, saving you time and reducing the chance of errors.",
        "When integrated with other AWS Console's pages that offers deep linking, you would just can't imagine all the possibilities that it might unblock.",
        "For example, you have an AWS System Manager managed EC2 instance that serves as the company VPN.",
        "Your team grows and the VPN instance often suffers from memory leak - and you know the best way is to reboot it. You have been running a Systems Manager runbook `AWS-RestartEC2Instance` for a while. One day you reserves an urgent request to fix the VPN as one of your business user is doing an external demo with an internal site that requires VPN connectivity.",
        "> Every second for the \"reboot fix\" matters.",
        "Instead of these long steps... 1. Log into AWS IAM Identity Center 2. Pick the right AWS Account 3. Pick the Access Role 4. Get into AWS Console 5. Navigate to EC2 Console 6. Pick your instance 7. Click \"Restart\"",
        "You can just 1. Click a new deep linked URL 2. Log into AWS IAM Identity Center 3. Execute a Systems Manager automation document with all parameters pre-filled: ``` https://us-east-1.console.aws.amazon.com/systems-manager/automation/execute/AWS-RestartEC2Instance? region=us-east-1#InstanceId= &AutomationAssumeRole= ```",
        "It's much more handy than the initial 7-step way!",
        "As an AWS Community Builder, I understand the critical importance of security, especially with this entry point of AWS Console. The IAM Identity Center Access Role Portal has been designed with security as a top priority:",
        "1. **Client-Side Only:** The entire application runs in the browser. There's no server-side component, which means there's no additional infrastructure to secure and maintain.",
        "2. **No Backend Data Storage:** The portal doesn't store any sensitive information. It's a pure interface tool that interacts directly with your AWS environment. Data is stored in your browser's local storage only.",
        "3. **Leverages Existing AWS Security:** By using AWS IAM Identity Center, the tool inherits AWS's robust security measures, such as multi-factor authentication and fine-grained permissions if you use AWS Identity Center's managed user directory.",
        "Because the Access Role Portal is a static web application, deployment is straightforward. You can just use my version as-is, or download the static files from my GitHub repository.",
        "Onboard your colleagues much faster",
        "With my tools' import & export functions, AWS account admins can prepare a list deep linked URLs of different accounts and console pages, export it and share to your organization's new joiners. Now they can get access to various team resources much easier, which just copy pasting:",
        "![Image description](/assets/img/fb48bd8872a2.png)",
        "Then, you no longer need to team your new colleague every time on \"how to access the CloudWatch Log group for that Lambda function?\" - You can just share them the exported URL and the will have the book marks imported in their portal already.",
        "Try [this sample exported link](https://iamidentitycenterroles.us-east-1.com#data=W3siaWQiOiIxNzIzMzUzNjA4Nzk3IiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImdhYnJpZWxrb28iLCJhY2NvdW50SWQiOiIxMzU3OTI0NjgxMDEiLCJyb2xlTmFtZSI6IkRlZXByYWNlciIsImRpc3BsYXlOYW1lIjoiRGVlcHJhY2VyQWNjZXNzIiwicmVkaXJlY3RVcmkiOiJodHRwczovL3VzLWVhc3QtMS5jb25zb2xlLmF3cy5hbWF6b24uY29tL2RlZXByYWNlci9ob21lP3JlZ2lvbj11cy1lYXN0LTEjbW9kZWxzIn0seyJpZCI6IjE3MjMzNTM5MjUxOTEiLCJpZGVudGl0eUNlbnRlckFsaWFzIjoiY29tcGFueWIiLCJhY2NvdW50SWQiOiIyMzQ1Njc4OTAxMjMiLCJyb2xlTmFtZSI6Ik5ldHdvcmtFbmdpbmVlciIsImRpc3BsYXlOYW1lIjoiTmV0d29ya0VuZ2luZWVyLUIiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtZWFzdC0xLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vdnBjL2hvbWU/cmVnaW9uPXVzLWVhc3QtMSJ9LHsiaWQiOiIxNzIzMzUzOTM2MDQzIiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImNvbXBhbnljIiwiYWNjb3VudElkIjoiMzQ1Njc4OTAxMjM0Iiwicm9sZU5hbWUiOiJTZWN1cml0eUFuYWx5c3QiLCJkaXNwbGF5TmFtZSI6IlNlY3VyaXR5LUMiLCJyZWRpcmVjdFVyaSI6Imh0dHBzOi8vdXMtd2VzdC0xLmNvbnNvbGUuYXdzLmFtYXpvbi5jb20vZ3VhcmRkdXR5L2hvbWU/cmVnaW9uPXVzLXdlc3QtMSJ9LHsiaWQiOiIxNzIzMzUzOTQ2MTI3IiwiaWRlbnRpdHlDZW50ZXJBbGlhcyI6ImNvbXBhbnlkIiwiYWNjb3VudElkIjoiNDU2Nzg5MDEyMzQ1Iiwicm9sZU5hbWUiOiJDbG91ZEFyY2hpdGVjdCIsImRpc3BsYXlOYW1lIjoiQXJjaGl0ZWN0LUQiLCJyZWRpcm",
        "The AWS IAM Identity Center Access Role Portal demonstrates how simple tools can significantly improve our cloud management workflows. By providing a clear, unified interface for role management, it saves time, reduces errors, and enhances security.",
        "As an AWS Community Builder, I'm committed to sharing knowledge and tools that help the community. I hope this portal will be useful for other AWS administrators dealing with multi-account environments.",
        "The code for this project is open-source and available on GitHub . I welcome contributions and feedback from the community to make this tool even better.",
        "Remember, effective IAM management is crucial for maintaining a secure and efficient AWS environment. Tools like this can help, but they should always be used in conjunction with AWS best practices and regular security audits."
      ]
    },
    {
      "id": "article:safeaws-checks-your-aws-cli-commands-before-they-are-run-3bjl",
      "source_type": "article",
      "title": "`safeaws` checks your AWS CLI commands before they are run",
      "url": "https://gabrielkoo.com/blog/safeaws-checks-your-aws-cli-commands-before-they-are-run-3bjl/",
      "canonical_url": "https://dev.to/aws-builders/safeaws-checks-your-aws-cli-commands-before-they-are-run-3bjl",
      "published_at": "2024-04-13",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "ai",
        "cloud",
        "cli"
      ],
      "description": "As a heavy AWS user, you probably know the struggle of needing to edit an existing AWS resource....",
      "content": "As a heavy AWS user, you probably know the struggle of needing to edit an existing AWS resource. Sometimes, the AWS CLI is just so much easier to use than the AWS Console, whether it's more programmatically friendly or the API isn't yet supported in the Console.\n\nYou end up copying a nicely drafted AWS CLI snippet from StackOverflow, or recently, one generated by a GenAI.\n\nAre you that confident the AWS API call would not make any breaking changes? Any potential side effects with your call? Have you familarized yourself with all arguments?\n\nThis is when I came up with the idea of the `safeaws` CLI wrapper:\n(Btw, I am pronouncing as “Safe”-“W”-“S”.):\n\n![safeaws cli demo GIF](/assets/img/0ec0cb54349b.gif)\n\n## How does it work\n\nIn short, it is a Python CLI wrapper around the famous `aws` CLI, that does the following:\n\n1. Use it like how you would normally run an AWS CLI command - just prefix `aws` with `safe` into `safeaws`.\n2. It fetches the CLI documentation with `aws <service> <cmd> help`, preprocesses it to remove less useful parts of the docs.\n3. A prompt is generated using the CLI documentation and the command you want to execute, which is then sent to Amazon Bedrock's Claude 3 model.\n4. The model will check for any potential issues/things to note to you;\n5. The CLI utility streams the checks to you in your shell.\n6. You are prompted to decide whether the AWS CLI command should still be run, after validating the checks.\n\n## Why pick the Claude 3 model on Amazon Bedrock?\n\nFirst of all, you are using `aws` CLI anyway, so it’s handy to further add little configuration to get Amazon Bedrock’s GenAI models called handily in your CLI.\n\nSecond, some AWS CLI docs are indeed long. For example, the documentation for `aws ec2 run-instances` command is around 17k tokens long. Only the latest models could accept prompts/chats with such a long context - Claude 3 models with their 200k context lengths are more than suitable.\n\nLastly, Claude 3 models are very cost effective and fast, at least when compared to our GPT-4 counterparts. You cannot achieve such a cost/speed ratio with other models.\n\n## Let’s run it!\n\nDownload the Python script and put it into your favorite `bin` directory, for example:\n\n```bash\n#!/bin/bash\n# It’s assumed your system Python3 has `boto3` installed.\nsudo curl https://raw.githubusercontent.com/gabrielkoo/safeaws-cli/main/safeaws.py \\\n  -o /usr/local/bin/safeaws && \\\nsudo chmod +x /usr/local/bin/safeaws\n```\n\nBe sure to validate its contents before using it. It’s always a good habit to do so for security:\n```bash\ncat /usr/local/bin/safeaws\n```\n\nNow just run your AWS command like you normally do, move your cursor to the beginning (`Opt + ←` on macOS) to prefix the `aws` command with `safe`:\n```\n# From\n> aws ec2 run-instances \\\n  —-image-id ami-12345678 \\\n  —-instance-family g6.48xlarge\n# To\n> safeaws ec2 run-instances \\\n  —-image-id ami-12345678 \\\n  —-instance-family g6.48xlarge\n\n# I made up the responses below, the actual call should be different\nIt looks like you want to launch a new EC2 instance, but please be aware of the following:\n\n1. g6.48xlarge seems to be really computationally powerful. Do you really need such a large instance? It would be very costly.\n\n2. You did not specify —-security-groups in your arguments, the default security group would be used and it might violate your organization’s security policy.\n\nTokens Usage:  Input: xxx, Output: yyy\n\n\nDo you want to execute the command? (y/N)\n```\n\n## Customization\n\nThe CLI wrapper could be configured with a few environment variables, namely the choice of AWS profile, which Claude model to use, max tokens as well as temperature. \n\nYou can also customize the GenAI prompt with your own checks, by modifying the `PROMPT_TEMPLATE` variable in the Python script.\n\n## Conclusion\n\nOf course, you won’t expect the GenAI to be 100% of the time right. At least in a few seconds and a very small cost (~US$0.005 per call, assuming each CLI docs is 20k tokens long), you got an industry-leading AI reviewing your world-changing AWS CLI calls - and it could have saved you from the trouble of recovering your wrongly modified/deleted AWS resources.\n\n## Source\n\nYou may find the source code of `safeaws` on my GitHub repo:\n<https://github.com/gabrielkoo/safeaws-cli>",
      "excerpts": [
        "As a heavy AWS user, you probably know the struggle of needing to edit an existing AWS resource. Sometimes, the AWS CLI is just so much easier to use than the AWS Console, whether it's more programmatically friendly or the API isn't yet supported in the Console.",
        "You end up copying a nicely drafted AWS CLI snippet from StackOverflow, or recently, one generated by a GenAI.",
        "Are you that confident the AWS API call would not make any breaking changes? Any potential side effects with your call? Have you familarized yourself with all arguments?",
        "This is when I came up with the idea of the `safeaws` CLI wrapper: (Btw, I am pronouncing as “Safe”-“W”-“S”.):",
        "![safeaws cli demo GIF](/assets/img/0ec0cb54349b.gif)",
        "In short, it is a Python CLI wrapper around the famous `aws` CLI, that does the following:",
        "1. Use it like how you would normally run an AWS CLI command - just prefix `aws` with `safe` into `safeaws`. 2. It fetches the CLI documentation with `aws help`, preprocesses it to remove less useful parts of the docs. 3. A prompt is generated using the CLI documentation and the command you want to execute, which is then sent to Amazon Bedrock's Claude 3 model. 4. The model will check for any potential issues/things to note to you; 5. The CLI utility streams the checks to you in your shell. 6. You are prompted to decide whether the AWS CLI command should still be run, after validating the checks.",
        "Why pick the Claude 3 model on Amazon Bedrock?",
        "First of all, you are using `aws` CLI anyway, so it’s handy to further add little configuration to get Amazon Bedrock’s GenAI models called handily in your CLI.",
        "Second, some AWS CLI docs are indeed long. For example, the documentation for `aws ec2 run-instances` command is around 17k tokens long. Only the latest models could accept prompts/chats with such a long context - Claude 3 models with their 200k context lengths are more than suitable.",
        "Lastly, Claude 3 models are very cost effective and fast, at least when compared to our GPT-4 counterparts. You cannot achieve such a cost/speed ratio with other models.",
        "Download the Python script and put it into your favorite `bin` directory, for example:",
        "Be sure to validate its contents before using it. It’s always a good habit to do so for security: ```bash cat /usr/local/bin/safeaws ```",
        "Now just run your AWS command like you normally do, move your cursor to the beginning (`Opt + ←` on macOS) to prefix the `aws` command with `safe`: ``` From > aws ec2 run-instances \\ —-image-id ami-12345678 \\ —-instance-family g6.48xlarge To > safeaws ec2 run-instances \\ —-image-id ami-12345678 \\ —-instance-family g6.48xlarge",
        "I made up the responses below, the actual call should be different It looks like you want to launch a new EC2 instance, but please be aware of the following:",
        "1. g6.48xlarge seems to be really computationally powerful. Do you really need such a large instance? It would be very costly.",
        "2. You did not specify —-security-groups in your arguments, the default security group would be used and it might violate your organization’s security policy.",
        "Tokens Usage: Input: xxx, Output: yyy",
        "Do you want to execute the command? (y/N) ```",
        "The CLI wrapper could be configured with a few environment variables, namely the choice of AWS profile, which Claude model to use, max tokens as well as temperature.",
        "You can also customize the GenAI prompt with your own checks, by modifying the `PROMPT_TEMPLATE` variable in the Python script.",
        "Of course, you won’t expect the GenAI to be 100% of the time right. At least in a few seconds and a very small cost (~US$0.005 per call, assuming each CLI docs is 20k tokens long), you got an industry-leading AI reviewing your world-changing AWS CLI calls - and it could have saved you from the trouble of recovering your wrongly modified/deleted AWS resources.",
        "You may find the source code of `safeaws` on my GitHub repo:"
      ]
    },
    {
      "id": "article:enhance-your-slack-workspace-with-a-user-trainable-chatgpt-integrated-faq-bot-2pj3",
      "source_type": "article",
      "title": "Enhance Your Slack Workspace with a user-trainable ChatGPT-Integrated FAQ Bot",
      "url": "https://gabrielkoo.com/blog/enhance-your-slack-workspace-with-a-user-trainable-chatgpt-integrated-faq-bot-2pj3/",
      "canonical_url": "https://dev.to/aws-builders/enhance-your-slack-workspace-with-a-user-trainable-chatgpt-integrated-faq-bot-2pj3",
      "published_at": "2023-04-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "chatgpt",
        "aws",
        "serverless"
      ],
      "description": "Update (2024-09-28) If you want a more powerful version that utilized Amazon Bedrock, as...",
      "content": "## Update (2024-09-28)\n\nIf you want a more powerful version that utilized Amazon Bedrock, as well as tools calling and a proper vector database, check out the newer version of this Slack chatbot [https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot](here)!\n\n## TLDR\n\nThis AWS & AI-powered chat bot solution costs around US$$0.009 per question only. And you may find an end-to-end tutorial on setting it up (in less than 30 minutes) at the end of this article.\n\n## Introduction\n\nHow much time do you spend on asking/answering questions about things that requires some internal context of your team/organization? Have you ever thought of an almighty AI that can does all the answering for you?\n\nIn this blog post, I'll walk you through the rationale of creating a user-trainable ChatGPT-integrated FAQ bot for your Slack workspace. \n\nThis bot will help boost your team's productivity by saving time on questioning, searching, and answering. By leveraging AWS services such as AWS SAM, AWS Lambda, and S3, we can create a cost-effective and efficient FAQ bot for your organization.\n\nLet's jump straight into a demo.\n\n## Demo\n\nThe demo bot is connected to episodes data from Wikipedia pages about the Disney+ TV Series \"The Mandalorian\".\n\nThe bot recognizes Grogu:\n![Who is Grogu](/assets/img/7f15aeed9fa0.png)\n\nBut it doesn't know who I am:\n![Who is Gabriel Koo](/assets/img/1ac6b4d59ffe.png)\n\nTo remedy this, I can submit a new article to the bot:\n(Note that it's still rare to see a ChatGPT/Embedding powered application to have such a \"direct feedback\" function as of now)\n![Submit a new article](/assets/img/a1472303aae9.png)\n\nNow the bot can answer my question:\n![Who is Gabriel Koo - answering with knowledge of the new article](/assets/img/b75b9dcab6e3.png)\n\nIt's cool isn't it?\n\n## Background\n\nChatGPT's API became available in March 2023, sparking excitement and numerous integrations all over the world. Two integrations that caught my attention are:\n\n1. [Combing Embedding Search with ChatGPT](https://github.com/openai/openai-cookbook/blob/main/examples/Question_answering_using_embeddings.ipynb) to build a FAQ engine - it's a way of:\n[Knowledge Base Question Answering (KBQA)](https://arxiv.org/pdf/2108.06688.pdf) to create a FAQ engine - this involves:\n  - Natural language understanding (using text embeddings for questions)\n  - Information retrieval (using text embeddings for articles, matching against the question's embeddings)\n  - Knowledge representation (using ChatGPT to select and present information)\n2. Connecting ChatGPT with a programmable messaging platform like Slack\n\nBut so far, I have not seen any open-source project that:\n\n1. combines the two together\n2. provides a easy hosting method like AWS SAM, and lastly\n3. provides a functionality to **let the user submit extra knowledge into the embedding dataset**.\n\nThe **third** point is crucial because, in the post-OpenAI era, a costly data scientist shouldn't be required to build a FAQ engine. Instead, users should contribute knowledge to the dataset, allowing the AI to learn from collective intelligence.\n\nSo, I built one myself.\n\n## It's 2023, Let's Go Serverless\n\nBack in 2016 when I was working in a consultancy company as an intern, I built and hosted a Neural Network (NN) based Slack chatbot on a (still free in 2016) dyno on Heroku to cater for questions like \"Where should we go for lunch?\" or \"Pick two colleagues to buy tea for us.\". The whole setup was very tedious: \n\n- Every time when I add a new sample question into the NN (and burning my laptop due to high CPU usage), I had to re-train the model locally again\n- I had also checked in the (big) model datafile into GitHub, so that Heroku would deploy the file as part of the codebase (and it took a long time for every deployment)\n\nThe bot was basic, using a classifier to determine the context with the highest probability and calling a hardcoded function accordingly.\n\nHowever, it's 2023 now. With models like ChatGPT and OpenAI's Embedding models, we can focus on user experience and architecture. That's why I've built the `/submit_train_article` command to let end users to further train the Slack bot in my application.\n\n## Why using AWS SAM?\n\nAWS SAM is an open source framework for building serverless applications on AWS. The greatest benefit is it provides shorthand syntax to express various cloud resources in a very simple way.\n\nNot only is it simple, it also does a lot of work for you under the hook such as\n\n- creating the right permission role for your serverless function so that it won't be a new attack service for your existing cloud resources.\n- building bundles for the required packages of your serverless function code\n\nLastly it's transferrable. There are so many open-sourced SAM projects on repositories like GitHub, so that you can just quickly experiment various solutions architected by others by deploying them easily - while skipping the need to provision every resources by yourself.\n\n## How it works\n\nYou may take a look at the diagrams below:\n\nThe architecture diagram:\n\n![Architecture diagram for this ChatGPT-powered FAQ Slack bot](/assets/img/0ddee39f9229.jpg)\n\nSequence diagram for the Q&A flow:\n\n![Question Flow for this ChatGPT-powered FAQ Slack bot](/assets/img/4bfbfe041b23.png)\n\nSequence diagram for the new training article submission flow:\n\n![Submit new article flow for this ChatGPT-powered FAQ Slack bot](/assets/img/c6e8c0190182.png)\n\nIn simple words, here's how it works:\n\n1. The question is converted into an \"Embedding\" with OpenAI's Embedding API\n2. The embedding of the question is compared against other embeddings generated from the FAQ article database\n3. The top few articles, together with the original question, is fed into ChatGPT\n4. ChatGPT gives a helpful answer to the question based on the relevant information, so that it would never answer you something like _\"I am sorry, but I do not have access to some info within your company\"_.\n\n## Architecture and Infrastructure\n\nThe entire integration is built using AWS SAM, consisting of the following two main components:\n\n- A Lambda function that handles the Slack API requests, it's possible with the new [Function URL](https://aws.amazon.com/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/) feature that was released in 2022. This saves us from the trouble of setting up an API Gateway.\n- An AWS S3 bucket to store the datafiles, that includes a CSV file of the articles, and a CSV file of the document embeddings.\n\nYeah that's it! With [AWS SAM](https://aws.amazon.com/serverless/sam/), things are simply so simple, and all these are defined in `template.yml`.\n\nIn just less than 100 lines, we could define all the infrastructure we need with AWS SAM:\n\n```yaml\nTransform: 'AWS::Serverless-2016-10-31'\n\nParameters:\n  OpenaiApiKey:\n    Type: String\n    Description: OpenAI API key\n    NoEcho: true\n  SlackBotToken:\n    Type: String\n    Description: Slack API token of your App, refer to \"Installed App\" settings, \"Bot User OAuth Token\" field.\n    NoEcho: true\n  SlackSigningSecret:\n    Type: String\n    Description: Signing secret of your Slack App, refer to \"Basic Information\" settings, \"App Credentials\" > \"Signing Secret\"\n    NoEcho: true\n\nResources:\n  Function:\n    Type: 'AWS::Serverless::Function'\n    Properties:\n      Runtime: python3.9\n      CodeUri: ./function\n      Handler: lambda_function.lambda_handler\n      Environment:\n        Variables:\n          OPENAI_API_KEY: !Ref OpenaiApiKey\n          SLACK_BOT_TOKEN: !Ref SlackBotToken\n          SLACK_SIGNING_SECRET: !Ref SlackSigningSecret\n          DATAFILE_S3_BUCKET: !Ref DataBucket\n      Layers:\n        - !Ref PythonLayer\n      MemorySize: 1024\n      Timeout: 15\n      Policies:\n        - AWSLambdaBasicExecutionRole\n      FunctionUrlConfig:\n        AuthType: NONE\n      Policies:\n        - S3CrudPolicy:\n            BucketName: !Ref DataBucket\n  PythonLayer:\n    Type: AWS::Serverless::LayerVersion\n    Properties:\n      ContentUri: ./layer\n    Metadata:\n      BuildMethod: python3.9\n  DataBucket:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName: !Sub '${FunctionName}-data'\n\nOutputs:\n  FunctionUrlEndpoint:\n    Description: 'Lambda Function URL Endpoint'\n    Value:\n      Fn::GetAtt: FunctionUrl.FunctionUrl\n``` \n(Truncated for simplicity, you can look at the full version [here](https://github.com/gabrielkoo/chatgpt-faq-slack-bot/blob/main/template.yml).)\n\nIn the template above, you can have:\n\n1. An AWS Lambda in 23 lines\n2. An AWS Lambda Layer in 6 lines(for the Python packages like OpenAI SDK)\n3. An S3 Bucket in 4 lines\n\nAnd a lot of unsung heroes are also created for you:\n1. CloudWatch logging configuration that stores the bot's logs\n2. An IAM Role that authorizes the AWS Lambda to\n  a. get and write S3 files in the data file bucket\n  b. write logs into CloudWatch Logs\n3. A publicly callable HTTP endpoint (So that you don't need to provision a virtual machine for a web server)\n\n## Serverless is Really Cool\n\nHere I would also like to express how excited it is to be able to setup such a smart FAQ chatbot in a Serverless way.\n\nUsing Lambda and Lambda Function URL as the web server S3 for data storage allows you to train your model locally and upload the latest models to the bucket. The Slack bot will then use the most recent dataset to serve your users. This approach is cost-effective, as it eliminates the need to provision a virtual machine for running a lightweight web server and avoids storing large model data files on the virtual machine.\n\nI did consider other storage options like DynamoDB or RDS, but these solutions are not as lightweight as S3, and that we always need the full embedding dataset to compute the \"article similarity\" so that both a SQL and NoSQL database doesn't help much. Though I did consider binding an Amazon Elastic File System (EFS) into the Lambda, the steps to set it up seems to be much larger than S3, so that's why I made the choice.\n\n## Cost Effective, Yet Powerful\n\nLet's assume you are using the current ChatGPT 3.5 pricing, even if you are using all 4k tokens in every pair of Q&A, with the question being ask having 500 tokens...\n\n```\nFor OpenAI's usage side:\n\nFor Embedding,\n  ($0.0004/1K tokens) * 1k tokens\n= $0.0004/question\n\nFor ChatGPT,\n  ($0.002/1K tokens) * 4k tokens \n= $0.008/question\n```\n\nOpenAI total $ per question: `$0.0084`\n\n```\nFor AWS's side:\nI assume that each call requires a 1GB RAM worker running for 10 seconds, with Lambda Function URL handling 50KB of data, and lastly the data file being 100MB in total.\n\nComputation\n  ($0.0000166667 for every GB-second) * 1GB * 10s\n= $0.000166667\n\nRequest\n  ($0.20 per 1M requests) * 1 request\n= $0.0000002\n\nS3 Retrieval\n  ($0.0004 per 1000 requests) * 1 request\n= $0.0000004\n\nP.S. Data transfer from S3 to Lambda is free (if they are in the same region)\n```\n\nAWS total $ per question: `$0.000167267`\n\nTotal $ per question: `$0.008567267`.\n\n\nThis is simply so inexpensive, especially the cost on AWS's side! It should be noted again, that this integration can give you a helpful answer in just 10 seconds! Say if you were looking for some information within your company's knowledge base, it could have taken you a few minutes to lead to a similar answer from the bot.\n\nWith the same budget, this AI-based chatbot is answering questions with a much fast speed and lower cost than a human assistant!\n\nOverall, this chatbot could be helping a lot of things when scaled up:\n- Replace the company internal knowledge base like Confluence or StackOverflow\n- Act as an assistant of your company's customer service team\n\nAnd the power of such a setup **won't stop growing** when your users submit more and more knowledge base articles into it.\n\n## Deploy it onto your Slack Workspace!\n\nAre you convinced that it's great to setup a bot like this? \n\nIf so, to set up your own ChatGPT-integrated FAQ bot, follow these steps:\n\n1. Clone the source code from my GitHub repository: <https://github.com/gabrielkoo/chatgpt-faq-slack-bot>\n2. Prepare your FAQ dataset in CSV format.\n3. Generate the embeddings file locally.\n4. Create your Slack app and install it into your Slack workspace.\n5. Deploy the SAM application using the provided template.\n6. Configure the Lambda Function URL in your Slack App's \"Request URL\" settings in Event Subscriptions and Slash Commands.\n\nIf your FAQ dataset is ready, you can complete the setup in just 30 minutes.\n\nTransform your team's productivity by reducing the time spent on questioning, searching, and answering with this user-trainable ChatGPT-integrated FAQ bot!\n\nP.S. This solution was also shared in an official [AWS Event](https://aws.amazon.com/local/hongkong/events/devday/#:~:text=Build%20and%20host%20your%20Serverless%20AI%2Dintegrated%20Slack%20Q%26A%20chatbot%20on%20AWS).\n\n\n## References\n\n- AWS Serverless Application Model <https://aws.amazon.com/serverless/aws-sam/>\n- OpenAI - \"Question answering using embeddings-based search\" <https://github.com/openai/openai-cookbook/blob/main/examples/Question_answering_using_embeddings.ipynb> - The entire logic of this bot came from this Python Notebook, except the \"user submit further training\" functionality.",
      "excerpts": [
        "If you want a more powerful version that utilized Amazon Bedrock, as well as tools calling and a proper vector database, check out the newer version of this Slack chatbot [https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot](here)!",
        "This AWS & AI-powered chat bot solution costs around US$$0.009 per question only. And you may find an end-to-end tutorial on setting it up (in less than 30 minutes) at the end of this article.",
        "How much time do you spend on asking/answering questions about things that requires some internal context of your team/organization? Have you ever thought of an almighty AI that can does all the answering for you?",
        "In this blog post, I'll walk you through the rationale of creating a user-trainable ChatGPT-integrated FAQ bot for your Slack workspace.",
        "This bot will help boost your team's productivity by saving time on questioning, searching, and answering. By leveraging AWS services such as AWS SAM, AWS Lambda, and S3, we can create a cost-effective and efficient FAQ bot for your organization.",
        "Let's jump straight into a demo.",
        "The demo bot is connected to episodes data from Wikipedia pages about the Disney+ TV Series \"The Mandalorian\".",
        "The bot recognizes Grogu: ![Who is Grogu](/assets/img/7f15aeed9fa0.png)",
        "But it doesn't know who I am: ![Who is Gabriel Koo](/assets/img/1ac6b4d59ffe.png)",
        "To remedy this, I can submit a new article to the bot: (Note that it's still rare to see a ChatGPT/Embedding powered application to have such a \"direct feedback\" function as of now) ![Submit a new article](/assets/img/a1472303aae9.png)",
        "Now the bot can answer my question: ![Who is Gabriel Koo - answering with knowledge of the new article](/assets/img/b75b9dcab6e3.png)",
        "ChatGPT's API became available in March 2023, sparking excitement and numerous integrations all over the world. Two integrations that caught my attention are:",
        "1. [Combing Embedding Search with ChatGPT](https://github.com/openai/openai-cookbook/blob/main/examples/Question_answering_using_embeddings.ipynb) to build a FAQ engine - it's a way of: [Knowledge Base Question Answering (KBQA)](https://arxiv.org/pdf/2108.06688.pdf) to create a FAQ engine - this involves: - Natural language understanding (using text embeddings for questions) - Information retrieval (using text embeddings for articles, matching against the question's embeddings) - Knowledge representation (using ChatGPT to select and present information) 2. Connecting ChatGPT with a programmable messaging platform like Slack",
        "But so far, I have not seen any open-source project that:",
        "1. combines the two together 2. provides a easy hosting method like AWS SAM, and lastly 3. provides a functionality to **let the user submit extra knowledge into the embedding dataset**.",
        "The **third** point is crucial because, in the post-OpenAI era, a costly data scientist shouldn't be required to build a FAQ engine. Instead, users should contribute knowledge to the dataset, allowing the AI to learn from collective intelligence.",
        "It's 2023, Let's Go Serverless",
        "Back in 2016 when I was working in a consultancy company as an intern, I built and hosted a Neural Network (NN) based Slack chatbot on a (still free in 2016) dyno on Heroku to cater for questions like \"Where should we go for lunch?\" or \"Pick two colleagues to buy tea for us.\". The whole setup was very tedious:",
        "- Every time when I add a new sample question into the NN (and burning my laptop due to high CPU usage), I had to re-train the model locally again - I had also checked in the (big) model datafile into GitHub, so that Heroku would deploy the file as part of the codebase (and it took a long time for every deployment)",
        "The bot was basic, using a classifier to determine the context with the highest probability and calling a hardcoded function accordingly.",
        "However, it's 2023 now. With models like ChatGPT and OpenAI's Embedding models, we can focus on user experience and architecture. That's why I've built the `/submit_train_article` command to let end users to further train the Slack bot in my application.",
        "AWS SAM is an open source framework for building serverless applications on AWS. The greatest benefit is it provides shorthand syntax to express various cloud resources in a very simple way.",
        "Not only is it simple, it also does a lot of work for you under the hook such as",
        "- creating the right permission role for your serverless function so that it won't be a new attack service for your existing cloud resources. - building bundles for the required packages of your serverless function code",
        "Lastly it's transferrable. There are so many open-sourced SAM projects on repositories like GitHub, so that you can just quickly experiment various solutions architected by others by deploying them easily - while skipping the need to provision every resources by yourself.",
        "You may take a look at the diagrams below:",
        "![Architecture diagram for this ChatGPT-powered FAQ Slack bot](/assets/img/0ddee39f9229.jpg)",
        "Sequence diagram for the Q&A flow:",
        "![Question Flow for this ChatGPT-powered FAQ Slack bot](/assets/img/4bfbfe041b23.png)",
        "Sequence diagram for the new training article submission flow:",
        "![Submit new article flow for this ChatGPT-powered FAQ Slack bot](/assets/img/c6e8c0190182.png)",
        "In simple words, here's how it works:",
        "1. The question is converted into an \"Embedding\" with OpenAI's Embedding API 2. The embedding of the question is compared against other embeddings generated from the FAQ article database 3. The top few articles, together with the original question, is fed into ChatGPT 4. ChatGPT gives a helpful answer to the question based on the relevant information, so that it would never answer you something like _\"I am sorry, but I do not have access to some info within your company\"_.",
        "Architecture and Infrastructure",
        "The entire integration is built using AWS SAM, consisting of the following two main components:",
        "- A Lambda function that handles the Slack API requests, it's possible with the new [Function URL](https://aws.amazon.com/blogs/aws/announcing-aws-lambda-function-urls-built-in-https-endpoints-for-single-function-microservices/) feature that was released in 2022. This saves us from the trouble of setting up an API Gateway. - An AWS S3 bucket to store the datafiles, that includes a CSV file of the articles, and a CSV file of the document embeddings.",
        "Yeah that's it! With [AWS SAM](https://aws.amazon.com/serverless/sam/), things are simply so simple, and all these are defined in `template.yml`.",
        "In just less than 100 lines, we could define all the infrastructure we need with AWS SAM:",
        "Parameters: OpenaiApiKey: Type: String Description: OpenAI API key NoEcho: true SlackBotToken: Type: String Description: Slack API token of your App, refer to \"Installed App\" settings, \"Bot User OAuth Token\" field. NoEcho: true SlackSigningSecret: Type: String Description: Signing secret of your Slack App, refer to \"Basic Information\" settings, \"App Credentials\" > \"Signing Secret\" NoEcho: true",
        "Resources: Function: Type: 'AWS::Serverless::Function' Properties: Runtime: python3.9 CodeUri: ./function Handler: lambda_function.lambda_handler Environment: Variables: OPENAI_API_KEY: !Ref OpenaiApiKey SLACK_BOT_TOKEN: !Ref SlackBotToken SLACK_SIGNING_SECRET: !Ref SlackSigningSecret DATAFILE_S3_BUCKET: !Ref DataBucket Layers: - !Ref PythonLayer MemorySize: 1024 Timeout: 15 Policies: - AWSLambdaBasicExecutionRole FunctionUrlConfig: AuthType: NONE Policies: - S3CrudPolicy: BucketName: !Ref DataBucket PythonLayer: Type: AWS::Serverless::LayerVersion Properties: ContentUri: ./layer Metadata: BuildMethod: python3.9 DataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub '${FunctionName}-data'",
        "Outputs: FunctionUrlEndpoint: Description: 'Lambda Function URL Endpoint' Value: Fn::GetAtt: FunctionUrl.FunctionUrl ``` (Truncated for simplicity, you can look at the full version [here](https://github.com/gabrielkoo/chatgpt-faq-slack-bot/blob/main/template.yml).)",
        "In the template above, you can have:",
        "1. An AWS Lambda in 23 lines 2. An AWS Lambda Layer in 6 lines(for the Python packages like OpenAI SDK) 3. An S3 Bucket in 4 lines",
        "And a lot of unsung heroes are also created for you: 1. CloudWatch logging configuration that stores the bot's logs 2. An IAM Role that authorizes the AWS Lambda to a. get and write S3 files in the data file bucket b. write logs into CloudWatch Logs 3. A publicly callable HTTP endpoint (So that you don't need to provision a virtual machine for a web server)",
        "Here I would also like to express how excited it is to be able to setup such a smart FAQ chatbot in a Serverless way.",
        "Using Lambda and Lambda Function URL as the web server S3 for data storage allows you to train your model locally and upload the latest models to the bucket. The Slack bot will then use the most recent dataset to serve your users. This approach is cost-effective, as it eliminates the need to provision a virtual machine for running a lightweight web server and avoids storing large model data files on the virtual machine.",
        "I did consider other storage options like DynamoDB or RDS, but these solutions are not as lightweight as S3, and that we always need the full embedding dataset to compute the \"article similarity\" so that both a SQL and NoSQL database doesn't help much. Though I did consider binding an Amazon Elastic File System (EFS) into the Lambda, the steps to set it up seems to be much larger than S3, so that's why I made the choice.",
        "Let's assume you are using the current ChatGPT 3.5 pricing, even if you are using all 4k tokens in every pair of Q&A, with the question being ask having 500 tokens...",
        "For Embedding, ($0.0004/1K tokens) * 1k tokens = $0.0004/question",
        "For ChatGPT, ($0.002/1K tokens) * 4k tokens = $0.008/question ```",
        "OpenAI total $ per question: `$0.0084`",
        "Computation ($0.0000166667 for every GB-second) * 1GB * 10s = $0.000166667",
        "Request ($0.20 per 1M requests) * 1 request = $0.0000002",
        "S3 Retrieval ($0.0004 per 1000 requests) * 1 request = $0.0000004",
        "P.S. Data transfer from S3 to Lambda is free (if they are in the same region) ```",
        "AWS total $ per question: `$0.000167267`",
        "Total $ per question: `$0.008567267`.",
        "This is simply so inexpensive, especially the cost on AWS's side! It should be noted again, that this integration can give you a helpful answer in just 10 seconds! Say if you were looking for some information within your company's knowledge base, it could have taken you a few minutes to lead to a similar answer from the bot.",
        "With the same budget, this AI-based chatbot is answering questions with a much fast speed and lower cost than a human assistant!",
        "Overall, this chatbot could be helping a lot of things when scaled up: - Replace the company internal knowledge base like Confluence or StackOverflow - Act as an assistant of your company's customer service team",
        "And the power of such a setup **won't stop growing** when your users submit more and more knowledge base articles into it.",
        "Deploy it onto your Slack Workspace!",
        "Are you convinced that it's great to setup a bot like this?",
        "If so, to set up your own ChatGPT-integrated FAQ bot, follow these steps:",
        "1. Clone the source code from my GitHub repository: 2. Prepare your FAQ dataset in CSV format. 3. Generate the embeddings file locally. 4. Create your Slack app and install it into your Slack workspace. 5. Deploy the SAM application using the provided template. 6. Configure the Lambda Function URL in your Slack App's \"Request URL\" settings in Event Subscriptions and Slash Commands.",
        "If your FAQ dataset is ready, you can complete the setup in just 30 minutes.",
        "Transform your team's productivity by reducing the time spent on questioning, searching, and answering with this user-trainable ChatGPT-integrated FAQ bot!",
        "P.S. This solution was also shared in an official [AWS Event](https://aws.amazon.com/local/hongkong/events/devday/#:~:text=Build%20and%20host%20your%20Serverless%20AI%2Dintegrated%20Slack%20Q%26A%20chatbot%20on%20AWS).",
        "- AWS Serverless Application Model - OpenAI - \"Question answering using embeddings-based search\" - The entire logic of this bot came from this Python Notebook, except the \"user submit further training\" functionality."
      ]
    },
    {
      "id": "article:do-not-use-default-sam-cloudformation-role-for-production-o5g",
      "source_type": "article",
      "title": "Do not use default SAM CloudFormation Role for Production",
      "url": "https://gabrielkoo.com/blog/do-not-use-default-sam-cloudformation-role-for-production-o5g/",
      "canonical_url": "https://dev.to/aws-builders/do-not-use-default-sam-cloudformation-role-for-production-o5g",
      "published_at": "2022-09-14",
      "last_verified_at": "2026-08-23",
      "tags": [
        "devops",
        "security",
        "aws",
        "sam"
      ],
      "description": "AWS CI/CD tools are simply cool, as they allow you to deploy your application within a few clicks,...",
      "content": "AWS CI/CD tools are simply cool, as they allow you to deploy your application within a few clicks, with a lot of supporting integrations and resources created for you.\n\nI could still recall how fascinated I was when I provisioned my first SAM project that includes a Rest API and an AWS Lambda backend within a few clicks ⚡️ - you have an API endpoint hosted already without the need to setup a web server by yourself.\n\nFor most of the cases, when you are creating a hello world project from the documentation, guides, some blogs or articles, it usually suggests you to use their suggested roles - which is actually **very risky** 🔓.\n\n## A potential trap with using Infrastructure as Code tools like SAM\n\nLet’s say I am creating a CD pipeline for deploying an [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) with `sam pipeline bootstrap`, following this piece of [documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-generating-example-ci-cd-codepipeline.html).\n\nThe guide is handy, as with a single command you can setup the entire CI/CD pipeline quickly, and you can also enjoy the benefit of having the latest features in your git branch deployed within a few minutes!\n\nMy focus here would be one of the arguments:\n\n> [`--cloudformation-execution-role TEXT`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-bootstrap.html#:~:text=%2D%2Dcloudformation%2Dexecution%2Drole)\tThe ARN of the IAM role to be assumed by the AWS CloudFormation service while deploying the application's stack. Provide only if you want to use your own role, otherwise the command will create one.\n\nThe description doesn't really warn you to use your own role, as well as any potential issues with leaving a default there. It will create a default role for you with the “default” permissions. This is actually disastrous for a production project, why? 🤔\n\n---\n\nWhen you run [`sam pipeline bootstrap`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-bootstrap.html) / [`sam pipeline init`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-init.html), a CloudFormation template is generated and deployed and it contains several resources to make your CI/CD pipeline provisioned for you.\n\nI created one with my Cloud9 shell, and it would look like this: \n![Resources created with sam cli](/assets/img/2caa3228483e.jpeg)\n\nAmong which, the [`CloudFormationExecutionRole`](https://github.com/aws/aws-sam-cli/blob/9a3aee3afff97e8562ada39787bb37eeec250fe2/samcli/lib/pipeline/bootstrap/stage_resources.yaml#L63-L85) is VERY DANGEROUS:\n```json\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Action\": \"*\",\n            \"Resource\": \"*\",\n            \"Effect\": \"Allow\"\n        }\n    ]\n}\n```\n\n\nIf you are not familiar with AWS IAM statements, it means this CloudFormation role can perform literally **EVERYTHING**, Including deleting everything in your account, or even provisioning any admin users, **on your behalf**; de facto an AWS version of `sudo` 🗝.\n\nSay when your source control is compromised (say if you are using GitHub and you leaked your personal access token), bad actors can commit any bad code into your SAM template and they can create whatever resource as defined in the CloudFormation role as they want - that's how Infrastructure as Code (IaC) works!\n\nOne of these would be simply be _define an IAM user with **hardcoded** password_.\n\nDoes this sound too bad to be true? Let me demonstrate it now.\n\n## A quick proof of concept\n\nIt’s easy to ignore such hidden traps until a disaster really occurs - and you will surely regret.\n\nSay for your repository, you forgot to setup default branch protection. Any colleague having access to your repo has push access to the `main` branch. Now let me try to use this vulnerability add an IAM user, with `AdministratorAccess`.\n\nIt’s as easy as adding the following CloudFormation / SAM resources in `template.yml`:\n\n```yaml\n# template.yml\n…\nResources:\n  NewAdminUser:\n    Type: AWS::IAM::User\n    Properties:\n      LoginProfile:\n        # SECURITY WARNING: DO NOT COPY!\n        Password: 'I-am-a-hardcoded-password'\n        PasswordResetRequired: false\n      Policies:\n        - PolicyName: AdminRight\n          PolicyDocument:\n            Version: '2012-10-17'\n            Statement:\n              - Sid: Whatever\n                Effect: Allow\n                Action: '*'\n                Resource: '*'\n```\n\nOnce this change is committed to git and picked by CodePipeline automatically 🤖, as part of the “legit” CI/CD process,\n![CI Pipeline doesn't check for malicious code by default](/assets/img/67ddf827d0dc.jpg)\n\nThe new admin user would be created:\n![Malicious admin user created with ease](/assets/img/6a786ac03bb8.jpg)\n\nAnd of course, the hacker would be able to login and do whatever they want with your account:\n\n![Backdoor to your organization, thanks to unhardened CloudFormation role](/assets/img/c2367aa14758.jpg)\n\nNow your organization’s AWS account is at risk, could it be leaked data of data loss, or even being injected any further vulnerabilities 💸.\n\n---\n\n## Remediations?\n\nAny one of the following four remediations could help avoid the vulnerability I illustrated above, but of course it would be best if every one of them are fulfilled ☔️:\n \n![Locate the places in the workflow that are vulnerable](/assets/img/44454b9725b2.jpeg)\n\n1. Protect your version control repository (e.g. git) well, setup branch protection, enforce code reviews before merging into your primary branch, setup proper permissions with least privilege, etc.\n\n2. To be super secure, add [an approval step](https://docs.aws.amazon.com/codepipeline/latest/userguide/approvals-action-add.html) if your are using CodePipeline. Conduct manual check again before every deployment in CI. Such a feature is also available for [`CircleCI`](https://circleci.com/blog/deploying-with-approvals/), and CI providers like [GitLab](https://docs.gitlab.com/ee/ci/environments/deployment_approvals.html) and [Jenkins](https://www.jenkins.io/doc/pipeline/steps/pipeline-input-step/) are also be catching up too. \n\n3. Continuously monitor and tweak your CI/CD roles with services [IAM Access Advisor]() or [IAM Access Analyzer](). It's okay to have a vulnerable role when you first provision the pipeline, but when your pipeline runs for a while these tool will help you to refine the necessary permission that you only need. In my example, I can tell from Access Advisor that the CloudFormation role only needs permissions for Lambda, CloudFormation, S3 and IAM: \n  ![Access Advisor tells your what your role exactly does](/assets/img/28e13c1a8d6d.jpg)\n\n4. Also as a general good practice, always look up the documentation, or try the [IAM policy JSON editor](https://aws.amazon.com/blogs/security/use-the-new-visual-editor-to-create-and-modify-your-aws-iam-policies/) in AWS Console. It will guide through you, for each IAM action, which kind of restrictions you can apply to your IAM Policy / Role. Always adopt the concept of least privilege to CI/CD users/roles to avoid any hacks from happening. For example if your pipeline needs to provision an SQS queue, you can try to allow only `sqs:CreateQueue` and `sqs:DeleteQueue` with specific queue names only. It will avoid the pipeline from being able to modify your other existing SQS queues:\n  ![Refining IAM statements to only the necessary resources](/assets/img/aa75dd2730f4.jpg)\n\n---\n\nP.S. If you are looking for a more \"official\" quick start for a CloudFormation role for SAM pipeline, the following example from AWS Samples might help:\n\nhttps://github.com/aws-samples/aws-serverless-samfarm/blob/474489cdfbd5a800e383be6eb2a4a87294626e13/pipeline/pipeline-roles.yaml#L55-L87\n\n\nIt is already much better than putting a CloudFormation role with full access rights.\n\n---\n\nCredits: Cover Photo by [George Becker](https://www.pexels.com/photo/monochrome-photography-of-keys-792031/).",
      "excerpts": [
        "AWS CI/CD tools are simply cool, as they allow you to deploy your application within a few clicks, with a lot of supporting integrations and resources created for you.",
        "I could still recall how fascinated I was when I provisioned my first SAM project that includes a Rest API and an AWS Lambda backend within a few clicks ⚡️ - you have an API endpoint hosted already without the need to setup a web server by yourself.",
        "For most of the cases, when you are creating a hello world project from the documentation, guides, some blogs or articles, it usually suggests you to use their suggested roles - which is actually **very risky** 🔓.",
        "A potential trap with using Infrastructure as Code tools like SAM",
        "Let’s say I am creating a CD pipeline for deploying an [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) with `sam pipeline bootstrap`, following this piece of [documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-generating-example-ci-cd-codepipeline.html).",
        "The guide is handy, as with a single command you can setup the entire CI/CD pipeline quickly, and you can also enjoy the benefit of having the latest features in your git branch deployed within a few minutes!",
        "My focus here would be one of the arguments:",
        "> [`--cloudformation-execution-role TEXT`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-bootstrap.html#:~:text=%2D%2Dcloudformation%2Dexecution%2Drole) The ARN of the IAM role to be assumed by the AWS CloudFormation service while deploying the application's stack. Provide only if you want to use your own role, otherwise the command will create one.",
        "The description doesn't really warn you to use your own role, as well as any potential issues with leaving a default there. It will create a default role for you with the “default” permissions. This is actually disastrous for a production project, why? 🤔",
        "When you run [`sam pipeline bootstrap`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-bootstrap.html) / [`sam pipeline init`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-pipeline-init.html), a CloudFormation template is generated and deployed and it contains several resources to make your CI/CD pipeline provisioned for you.",
        "I created one with my Cloud9 shell, and it would look like this: ![Resources created with sam cli](/assets/img/2caa3228483e.jpeg)",
        "Among which, the [`CloudFormationExecutionRole`](https://github.com/aws/aws-sam-cli/blob/9a3aee3afff97e8562ada39787bb37eeec250fe2/samcli/lib/pipeline/bootstrap/stage_resources.yaml#L63-L85) is VERY DANGEROUS: ```json { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Action\": \"*\", \"Resource\": \"*\", \"Effect\": \"Allow\" } ] } ```",
        "If you are not familiar with AWS IAM statements, it means this CloudFormation role can perform literally **EVERYTHING**, Including deleting everything in your account, or even provisioning any admin users, **on your behalf**; de facto an AWS version of `sudo` 🗝.",
        "Say when your source control is compromised (say if you are using GitHub and you leaked your personal access token), bad actors can commit any bad code into your SAM template and they can create whatever resource as defined in the CloudFormation role as they want - that's how Infrastructure as Code (IaC) works!",
        "One of these would be simply be _define an IAM user with **hardcoded** password_.",
        "Does this sound too bad to be true? Let me demonstrate it now.",
        "It’s easy to ignore such hidden traps until a disaster really occurs - and you will surely regret.",
        "Say for your repository, you forgot to setup default branch protection. Any colleague having access to your repo has push access to the `main` branch. Now let me try to use this vulnerability add an IAM user, with `AdministratorAccess`.",
        "It’s as easy as adding the following CloudFormation / SAM resources in `template.yml`:",
        "Once this change is committed to git and picked by CodePipeline automatically 🤖, as part of the “legit” CI/CD process, ![CI Pipeline doesn't check for malicious code by default](/assets/img/67ddf827d0dc.jpg)",
        "The new admin user would be created: ![Malicious admin user created with ease](/assets/img/6a786ac03bb8.jpg)",
        "And of course, the hacker would be able to login and do whatever they want with your account:",
        "![Backdoor to your organization, thanks to unhardened CloudFormation role](/assets/img/c2367aa14758.jpg)",
        "Now your organization’s AWS account is at risk, could it be leaked data of data loss, or even being injected any further vulnerabilities 💸.",
        "Any one of the following four remediations could help avoid the vulnerability I illustrated above, but of course it would be best if every one of them are fulfilled ☔️: ![Locate the places in the workflow that are vulnerable](/assets/img/44454b9725b2.jpeg)",
        "1. Protect your version control repository (e.g. git) well, setup branch protection, enforce code reviews before merging into your primary branch, setup proper permissions with least privilege, etc.",
        "2. To be super secure, add [an approval step](https://docs.aws.amazon.com/codepipeline/latest/userguide/approvals-action-add.html) if your are using CodePipeline. Conduct manual check again before every deployment in CI. Such a feature is also available for [`CircleCI`](https://circleci.com/blog/deploying-with-approvals/), and CI providers like [GitLab](https://docs.gitlab.com/ee/ci/environments/deployment_approvals.html) and [Jenkins](https://www.jenkins.io/doc/pipeline/steps/pipeline-input-step/) are also be catching up too.",
        "3. Continuously monitor and tweak your CI/CD roles with services [IAM Access Advisor]() or [IAM Access Analyzer](). It's okay to have a vulnerable role when you first provision the pipeline, but when your pipeline runs for a while these tool will help you to refine the necessary permission that you only need. In my example, I can tell from Access Advisor that the CloudFormation role only needs permissions for Lambda, CloudFormation, S3 and IAM: ![Access Advisor tells your what your role exactly does](/assets/img/28e13c1a8d6d.jpg)",
        "4. Also as a general good practice, always look up the documentation, or try the [IAM policy JSON editor](https://aws.amazon.com/blogs/security/use-the-new-visual-editor-to-create-and-modify-your-aws-iam-policies/) in AWS Console. It will guide through you, for each IAM action, which kind of restrictions you can apply to your IAM Policy / Role. Always adopt the concept of least privilege to CI/CD users/roles to avoid any hacks from happening. For example if your pipeline needs to provision an SQS queue, you can try to allow only `sqs:CreateQueue` and `sqs:DeleteQueue` with specific queue names only. It will avoid the pipeline from being able to modify your other existing SQS queues: ![Refining IAM statements to only the necessary resources](/assets/img/aa75dd2730f4.jpg)",
        "P.S. If you are looking for a more \"official\" quick start for a CloudFormation role for SAM pipeline, the following example from AWS Samples might help:",
        "https://github.com/aws-samples/aws-serverless-samfarm/blob/474489cdfbd5a800e383be6eb2a4a87294626e13/pipeline/pipeline-roles.yaml#L55-L87",
        "It is already much better than putting a CloudFormation role with full access rights.",
        "Credits: Cover Photo by [George Becker](https://www.pexels.com/photo/monochrome-photography-of-keys-792031/)."
      ]
    },
    {
      "id": "article:productivity-hacks-with-bookmarklets",
      "source_type": "article",
      "title": "Write your own bookmarklet, the tiny program that yields productivity hacks!",
      "url": "https://gabrielkoo.com/blog/productivity-hacks-with-bookmarklets/",
      "canonical_url": "https://gabrielkoo.com/blog/productivity-hacks-with-bookmarklets/",
      "published_at": "2021-06-06",
      "last_verified_at": "2026-08-23",
      "tags": [
        "bookmarklet",
        "automation",
        "productivity",
        "javascript",
        "chrome",
        "safari",
        "firefox"
      ],
      "description": "Learn about bookmarklet, the tiny program that runs inside your browser's bookmarks bar.",
      "content": "People always perform their work on web browsers. You might be performing some repetitive steps every day during work, and you might have wondered - it would be really nice if I can install a browser extension that simplify my works with a single click!\n\nYeah, there are tons of browser extensions available, but what if you work in a company that prohibits you from installing browser extensions that are not approved? IT departments usually ban the installation of unverified extensions for a reason. I randomly found an article from Google - <https://www.zdnet.com/article/three-million-users-installed-28-malicious-chrome-or-edge-extensions/>, browser extensions could be dangerous.\n\nThen recently, I came across the idea of using \"[bookmarklets](https://en.wikipedia.org/wiki/Bookmarklet)\" from my colleague:\n\n> *A bookmarklet is a bookmark stored in a web browser that contains JavaScript commands that add new features to the browser.*\n\nI'll illustrate what is a bookmarklet in a few examples.\n\n## Listing Google search results of the current page - with a single click\n\nSay you are an SEO specialist and one of your daily job is to look at which URLs of your domain appear on Google:\n\n![Search domain on Google](/assets/img/search-domain-on-google.gif)\n\nSource code? Actually not that complex:\n\n```javascript\njavascript:window.open('https://www.google.com/search?q=site:'+window.location.host)\n```\n\nWhile the code itself is very short, it actually represents a **workflow** of what an SEO specialist might always do:\n\n1. Remember / copy the full domain name of a website, say *somedomain.com*\n2. Open a new browser tab\n3. Enters *google.com*\n4. In the search bar, enter the token *site:*\n5. Paste / type the domain, so that the search field becomes e.g. *site:somedomain.com,* and then press enter\n\nIf one is normally using **5 seconds** to do that, now you can run the whole \"workflow\" within **half a second**. If one does that check once every working day, when annualised the bookmarklet is saving you **(5 - 0.5)** × **20** × **12 = 1,080 seconds = 18 minutes**.\n\nWhile this might sound totally useless to you, I see an opportunity to speed up a lot of our tiny chore tasks. What you need is imagination!\n\n![imagination](https://media.giphy.com/media/xRJZH4Ajr973y/source.gif)\n\nLet's look at two more examples I just thought of:\n\n### Run PageSpeed Insights for current page\n\nAnother \"workflow\" that a website owner might always do - check the loading speed of your website against Google pageSpeed Insights. Again, it is a simple one, but it did save your time right?\n\n![Run Google PageSpeed Insights for current page](/assets/img/run-pagespeed-insights-for-current-page.gif)\n\n```javascript\njavascript:window.open('https://developers.google.com/speed/pagespeed/insights/?url='+window.location)\n```\n\n### Search for the selected text within your Gmail\n\nPerform a search on Gmail for the highlighted text in any webpage.\n\n![Search on Gmail for selected text on any webpage](/assets/img/search-highlighted-text-in-gmail.gif)\n\n```javascript\njavascript:window.open('https://mail.google.com/mail/u/0/#search/'+window.getSelection().toString())\n```\n\n### So how to write my own bookmarklet?\n\nI was using this project: <https://caiorss.github.io/bookmarklet-maker/>\n\nWrite your own code, and then the tool will URL encode your JavaScript into the URI format:\n\n![Bookmarklet generator](/assets/img/bookmarklet-maker.png)\n\nLastly, paste your converted code into the \"URL\" field when you create a new bookmark.\n\n![Save bookmarklet as a new bookmark](/assets/img/save-bookmarklet.png)\n\nThat's it!\n\n### It's cool, but I don't see any particular benefits?\n\nTo me, there are a few things I like about bookmarklets:\n\n* \"Cross-platform\" and could be distributed easily\n  - Most (modern) browsers speak JavaScript. Actually, bookmarklets works on mobile browsers too!\n* Less evil than a typical browser plugin\n  - you have to deep dive to verify it's really safe\n* Easier to understand\n  - as compared to VBA / .exe (very personal opinion)\n* Leverage a lot of browser APIs or even integrate with 3rd party services\n  - though that depends on the security setup of each website (e.g. security headers like CSP)\n* Send your data to a lot of custom URI schemes, e.g.\n  - `mailto:hi@gabrielkoo.com?subject=Thanks+for+sharing+about+bookmarklets!&body=<some-data-from-javascript>`;\n  - or URIs for mobile apps.\n\nP.S. Who am I? I am actually a Full Stack / DevSecOps Engineer, but occasionally I do help setup some productivity hacks for my company. Recently I built a hot desk booking system for my company's new studio, with live desk vacancy preview and full booking log - all powered on Google Workspace with minimal Apps Script.",
      "excerpts": [
        "People always perform their work on web browsers. You might be performing some repetitive steps every day during work, and you might have wondered - it would be really nice if I can install a browser extension that simplify my works with a single click!",
        "Yeah, there are tons of browser extensions available, but what if you work in a company that prohibits you from installing browser extensions that are not approved? IT departments usually ban the installation of unverified extensions for a reason. I randomly found an article from Google - , browser extensions could be dangerous.",
        "Then recently, I came across the idea of using \"[bookmarklets](https://en.wikipedia.org/wiki/Bookmarklet)\" from my colleague:",
        "> *A bookmarklet is a bookmark stored in a web browser that contains JavaScript commands that add new features to the browser.*",
        "I'll illustrate what is a bookmarklet in a few examples.",
        "Listing Google search results of the current page - with a single click",
        "Say you are an SEO specialist and one of your daily job is to look at which URLs of your domain appear on Google:",
        "![Search domain on Google](/assets/img/search-domain-on-google.gif)",
        "Source code? Actually not that complex:",
        "While the code itself is very short, it actually represents a **workflow** of what an SEO specialist might always do:",
        "1. Remember / copy the full domain name of a website, say *somedomain.com* 2. Open a new browser tab 3. Enters *google.com* 4. In the search bar, enter the token *site:* 5. Paste / type the domain, so that the search field becomes e.g. *site:somedomain.com,* and then press enter",
        "If one is normally using **5 seconds** to do that, now you can run the whole \"workflow\" within **half a second**. If one does that check once every working day, when annualised the bookmarklet is saving you **(5 - 0.5)** × **20** × **12 = 1,080 seconds = 18 minutes**.",
        "While this might sound totally useless to you, I see an opportunity to speed up a lot of our tiny chore tasks. What you need is imagination!",
        "![imagination](https://media.giphy.com/media/xRJZH4Ajr973y/source.gif)",
        "Let's look at two more examples I just thought of:",
        "Run PageSpeed Insights for current page",
        "Another \"workflow\" that a website owner might always do - check the loading speed of your website against Google pageSpeed Insights. Again, it is a simple one, but it did save your time right?",
        "![Run Google PageSpeed Insights for current page](/assets/img/run-pagespeed-insights-for-current-page.gif)",
        "Search for the selected text within your Gmail",
        "Perform a search on Gmail for the highlighted text in any webpage.",
        "![Search on Gmail for selected text on any webpage](/assets/img/search-highlighted-text-in-gmail.gif)",
        "So how to write my own bookmarklet?",
        "Write your own code, and then the tool will URL encode your JavaScript into the URI format:",
        "![Bookmarklet generator](/assets/img/bookmarklet-maker.png)",
        "Lastly, paste your converted code into the \"URL\" field when you create a new bookmark.",
        "![Save bookmarklet as a new bookmark](/assets/img/save-bookmarklet.png)",
        "It's cool, but I don't see any particular benefits?",
        "To me, there are a few things I like about bookmarklets:",
        "* \"Cross-platform\" and could be distributed easily - Most (modern) browsers speak JavaScript. Actually, bookmarklets works on mobile browsers too! * Less evil than a typical browser plugin - you have to deep dive to verify it's really safe * Easier to understand - as compared to VBA / .exe (very personal opinion) * Leverage a lot of browser APIs or even integrate with 3rd party services - though that depends on the security setup of each website (e.g. security headers like CSP) * Send your data to a lot of custom URI schemes, e.g. - `mailto:hi@gabrielkoo.com?subject=Thanks+for+sharing+about+bookmarklets!&body= `; - or URIs for mobile apps.",
        "P.S. Who am I? I am actually a Full Stack / DevSecOps Engineer, but occasionally I do help setup some productivity hacks for my company. Recently I built a hot desk booking system for my company's new studio, with live desk vacancy preview and full booking log - all powered on Google Workspace with minimal Apps Script."
      ]
    },
    {
      "id": "article:edge-functions",
      "source_type": "article",
      "title": "SEO / Speed Enhancements with Edge Functions (With Examples)",
      "url": "https://gabrielkoo.com/blog/edge-functions/",
      "canonical_url": "https://gabrielkoo.com/blog/edge-functions/",
      "published_at": "2021-01-23",
      "last_verified_at": "2026-08-23",
      "tags": [
        "csp",
        "edge-function",
        "cloudflare",
        "cloudfront",
        "lambda",
        "structured-data"
      ],
      "description": "Learn how edge functions like CloudFlare Workers or AWS CloudFront Lambda@Edge can help the SEO of your website, besides caching and speed enhancements.",
      "content": "If you are using a CMS / static hosting website to host your site, there may be a lot of restrictions.\n\nYou might want to add some request headers, apply some optimizations. However, all these might not be possible with tools like [WordPress.org](wordpress.org) or GitHub pages.\n\nWhat can you do with it?\n\nThe answer is Edge functions. Two examples that I have experience is AWS CloudFront’s Lambda@Edge functions and CloudFlare workers.\n\nIf you want to experience with it, you might transfer your DNS onto  CloudFlare. First 100,000 Workers requests for each day is free!\n\nSo below I will include some CloudFlare worker script examples for such purposes.\n\n## Preload / Prefetch Assets\n\n```javascript\nlet body = await response.text();\nconst scriptRegExp = /<script[^>]*?src=“(.*?)”[^>]*?><\\/script>/g;\n\nlet links = [];\nfor (var match in body.matchAll(scriptRegExp)) {\n  let scriptUrl = match[1];\n  links.push(`<${scriptUrl}; as=script; rel=preload>`);  // the first element is the whole script tag\n}\n\n/* Then assign the `links` into the response‘s `Link` header. */\n```\n\n## Add Security Headers\n\nFor example, you can generate CSP header AFTER parsing your raw HTML content. If you do work on CSP, you must know that working with the CSP with scripts is not an easy task.\n\n## Image Lazy Loading\n\nGoogle suggests that image lazy loading is a good and quick enhancement to your website.\n\nIt would be straightforward to e.g. apply the `class=“lazy” data-src=“<original-url>”` attributes in `<img/>` elements like the CSP example.\n\n## Automatically Add Other HTML Tags\n\nIt’s possible to generate some SEO tags, such as `<link/>` canonical tags, or `ld+json` script tags.\n* canonical\n* hreflang\n\nThe request headers such as host, origin or referer could be used as inputs for generating these tags.\n\nSounds difficult but still want to apply it to your business?\n\nContact me at [email](mailto:hi@gabrielkoo.com). I offer paid services that this :).",
      "excerpts": [
        "If you are using a CMS / static hosting website to host your site, there may be a lot of restrictions.",
        "You might want to add some request headers, apply some optimizations. However, all these might not be possible with tools like [WordPress.org](wordpress.org) or GitHub pages.",
        "The answer is Edge functions. Two examples that I have experience is AWS CloudFront’s Lambda@Edge functions and CloudFlare workers.",
        "If you want to experience with it, you might transfer your DNS onto CloudFlare. First 100,000 Workers requests for each day is free!",
        "So below I will include some CloudFlare worker script examples for such purposes.",
        "let links = []; for (var match in body.matchAll(scriptRegExp)) { let scriptUrl = match[1]; links.push(` `); // the first element is the whole script tag }",
        "/* Then assign the `links` into the response‘s `Link` header. */ ```",
        "For example, you can generate CSP header AFTER parsing your raw HTML content. If you do work on CSP, you must know that working with the CSP with scripts is not an easy task.",
        "Google suggests that image lazy loading is a good and quick enhancement to your website.",
        "It would be straightforward to e.g. apply the `class=“lazy” data-src=“ ”` attributes in ` ` elements like the CSP example.",
        "Automatically Add Other HTML Tags",
        "It’s possible to generate some SEO tags, such as ` ` canonical tags, or `ld+json` script tags. * canonical * hreflang",
        "The request headers such as host, origin or referer could be used as inputs for generating these tags.",
        "Sounds difficult but still want to apply it to your business?",
        "Contact me at [email](mailto:hi@gabrielkoo.com). I offer paid services that this :)."
      ]
    },
    {
      "id": "article:covid-19-clusters",
      "source_type": "article",
      "title": "My Tiny Easter Project on Hong Kong's COVID-19 Clusters",
      "url": "https://gabrielkoo.com/blog/covid-19-clusters/",
      "canonical_url": "https://gabrielkoo.com/blog/covid-19-clusters/",
      "published_at": "2020-04-13",
      "last_verified_at": "2026-08-23",
      "tags": [
        "covid-19",
        "covid19",
        "coronavirus",
        "hong kong"
      ],
      "description": "I obtained the publicly available crowdsourced data behind the famous open-sourced COVID-19 information site in Hong Kong, wars.vote4.hk, and started some cleaning on the data.",
      "content": "I obtained the publicly available crowdsourced data behind the famous open-sourced COVID-19 information site in Hong Kong, [wars.vote4.hk](https://wars.vote4.hk/), and started some cleaning on the data.\n\nFor each confirmed case, a brief summary (unfortunately in plain text) of the patient, for example, where did the patient go, is available. The summary could also include the case numbers of the persons that the patient was in contact with. Such transparency on the data might not have been available for some of the places in the world that are suffering from the disease, given the huge number of cases in those areas and the lack of records.\n\nAnd then I started writing a tiny Python script, with the help of some simple tricks including regular expressions to build the graph data from the raw confirmed case records. Cases that are interrelated are grouped together into clusters, and I subsequently added labels by associating the larger clusters into the named ones that were mentioned on the news. The data parsing work was not as easy as I think, because the data format was not that consistent. This made me recall an observation that my supervisor has made:\n\n> #### Data engineering usually takes more time than data analysis itself.\n\n```python\n# Match all the \"Bars and Bands\" cases, while catering different types of variations\nr'bars? (?:and|\\&) bands'\n\n# Catching Chinese phrases containing the case numbers\nr'第(?:[ 、及\\d]+)宗'\n```\n\nOnce the cluster dataset was ready, I started building a small webpage on [React](https://reactjs.org/) with the help of [Material UI](https://material-ui.com/) and [react-d3-graph](https://github.com/danielcaldas/react-d3-graph). In fact, that didn't take as much time as data cleaning. Note I didn't set up an API for fetching the data since all in all, it required some (a bit heavy) offline pre-processing. The page includes the cluster chart itself, together with a simple dropdown field to let myself filter for a named cluster or to search for a cluster containing a particular case.\n\nWhile I would not describe my work as really insightful or useful, there were indeed some findings that I wouldn't have realized in case I only looked at the original data in its tabular form. For example, I didn't know that the clusters *\"CEO Neway\"* / *\"Marks & Spencer\"* were in fact linked to the *\"Bars and Bands\"* cluster in Lan Kwai Fong:\n\n![Screenshot for the Lan Kwai Fong cluster](/assets/img/cluster.png)\n\nIn fact, the whole exercise didn't require very advanced techniques in both data analytics and frontend, but it is already good enough to let us have a quick look at the interrelationships between the cases. But more importantly, I feel that I have made good use of my Easter holiday to do something I love!\n\nLastly, if you are interested, you may find the published site here, with the automated deployment and update of data every 24 hours: [covid19.gabrielkoo.com](https://covid19.gabrielkoo.com)\n\nThe original post was first published on [Linkedin](https://www.linkedin.com/pulse/my-tiny-easter-project-hong-kongs-covid-19-clusters-gabriel-koo/).",
      "excerpts": [
        "I obtained the publicly available crowdsourced data behind the famous open-sourced COVID-19 information site in Hong Kong, [wars.vote4.hk](https://wars.vote4.hk/), and started some cleaning on the data.",
        "For each confirmed case, a brief summary (unfortunately in plain text) of the patient, for example, where did the patient go, is available. The summary could also include the case numbers of the persons that the patient was in contact with. Such transparency on the data might not have been available for some of the places in the world that are suffering from the disease, given the huge number of cases in those areas and the lack of records.",
        "And then I started writing a tiny Python script, with the help of some simple tricks including regular expressions to build the graph data from the raw confirmed case records. Cases that are interrelated are grouped together into clusters, and I subsequently added labels by associating the larger clusters into the named ones that were mentioned on the news. The data parsing work was not as easy as I think, because the data format was not that consistent. This made me recall an observation that my supervisor has made:",
        "> #### Data engineering usually takes more time than data analysis itself.",
        "Catching Chinese phrases containing the case numbers r'第(?:[ 、及\\d]+)宗' ```",
        "Once the cluster dataset was ready, I started building a small webpage on [React](https://reactjs.org/) with the help of [Material UI](https://material-ui.com/) and [react-d3-graph](https://github.com/danielcaldas/react-d3-graph). In fact, that didn't take as much time as data cleaning. Note I didn't set up an API for fetching the data since all in all, it required some (a bit heavy) offline pre-processing. The page includes the cluster chart itself, together with a simple dropdown field to let myself filter for a named cluster or to search for a cluster containing a particular case.",
        "While I would not describe my work as really insightful or useful, there were indeed some findings that I wouldn't have realized in case I only looked at the original data in its tabular form. For example, I didn't know that the clusters *\"CEO Neway\"* / *\"Marks & Spencer\"* were in fact linked to the *\"Bars and Bands\"* cluster in Lan Kwai Fong:",
        "![Screenshot for the Lan Kwai Fong cluster](/assets/img/cluster.png)",
        "In fact, the whole exercise didn't require very advanced techniques in both data analytics and frontend, but it is already good enough to let us have a quick look at the interrelationships between the cases. But more importantly, I feel that I have made good use of my Easter holiday to do something I love!",
        "Lastly, if you are interested, you may find the published site here, with the automated deployment and update of data every 24 hours: [covid19.gabrielkoo.com](https://covid19.gabrielkoo.com)",
        "The original post was first published on [Linkedin](https://www.linkedin.com/pulse/my-tiny-easter-project-hong-kongs-covid-19-clusters-gabriel-koo/)."
      ]
    },
    {
      "id": "article:gatsby-netlify-csp-headers",
      "source_type": "article",
      "title": "CSP Headers for Gatsby project on Netlify",
      "url": "https://gabrielkoo.com/blog/gatsby-netlify-csp-headers/",
      "canonical_url": "https://gabrielkoo.com/blog/gatsby-netlify-csp-headers/",
      "published_at": "2019-09-28",
      "last_verified_at": "2026-08-23",
      "tags": [
        "content security policy",
        "csp",
        "gatsby",
        "gatsby-plugin-csp",
        "gatsby-plugin-netlify",
        "mozilla observatory"
      ],
      "description": "Content Security Policy (CSP) is really important, but troublesome too...",
      "content": "So I want CSP to be implemented.\n\nIn fact, there is already one official plugin that can help you do this:\n\n**[gatsby-plugin-csp](https://www.gatsbyjs.org/packages/gatsby-plugin-csp/)**\n\nWell done, it even generates the CSP `sha-256` CSP hashes for you. But that's not perfect, since it only injects the `http-equiv` `<meta/>` tag for you. In case you are a fan of CSP, you must know that [Observatory](https://observatory.mozilla.org) does not support CSP in meta tags for now. Anyway, setting CSP in HTTP headers is better since there are some tags (e.g. `frame-ancestors` could not be set in `<meta/>`.\n\nOn the other hand, (oh by the way this site is hosted with `Netlify`), there is a Gatsby plugin for generating some Netlify-specific \"server-side\" config scripts, namely `_headers` and `_redirects` :\n\n**[gatsby-plugin-netlify](https://www.gatsbyjs.org/packages/gatsby-plugin-netlify/)**\n\nThen I found that the `transformHeaders: (headers, path)` option seems to be configurable for my use case.\n\nSo what does my hack do?\n\n1. the `gatsby-plugin-csp` generates the CSP hashes, build the CSP string and inject it as `<meta/>` in *each* generated HTML file, in the `./public` directory by default.\n2. the `gatsby-plugin-netlify` loops over each path, and the `transformHeaders` method is called upon processing of each page. You can manually update the `headers` argument according to the `path` argument. This is where the hack happens:\n   - read each generated HTML file\n   - extract the `content` value of the tag `<meta name=\"Content-Security-policy\" />`\n   - update the variable `headers` with the CSP header\n   - remove the CSP meta tag in the original HTML file\n\nThat's all! No need to hack the original two plugins at all while you don't have to struggle writing a new Gatsby plugin from scratch.\n\n```javascript\n/* gatsby-config.js */\n\nmodule.exports = {\n  plugins: [\n    {\n      resolve: `gatsby-plugin-csp`,\n      options: {\n        disableOnDev: false,\n        reportOnly: false,\n        mergeScriptHashes: true,\n        mergeStyleHashes: true,\n        mergeDefaultDirectives: true,\n      },\n    },\n    {\n      resolve: `gatsby-plugin-netlify`,\n      options: {\n        transformHeaders: (headers, path) => {\n          if (path.endsWith('/')) {\n            const filePath = `./public${path}index.html`;\n            const rawHtml = readFileSync(filePath).toString();\n            const csp = /<meta http-equiv=\"Content-Security-Policy\" content=\"(.*?)\"\\/>/.exec(rawHtml)[1].replace(/&#x27;/g, `'`);\n            headers.push(`Content-Security-Policy: ${csp}`);\n            writeFileSync(filePath, rawHtml.replace(/<meta http-equiv=\"Content-Security-Policy\" content=\".*?\"\\/>/g, ''));\n          }\n          return headers;\n        },\n        mergeSecurityHeaders: true,\n        mergeLinkHeaders: true,\n        mergeCachingHeaders: true,\n        generateMatchPathRewrites: true,\n      },\n    },\n  ],\n};\n```",
      "excerpts": [
        "So I want CSP to be implemented.",
        "In fact, there is already one official plugin that can help you do this:",
        "**[gatsby-plugin-csp](https://www.gatsbyjs.org/packages/gatsby-plugin-csp/)**",
        "Well done, it even generates the CSP `sha-256` CSP hashes for you. But that's not perfect, since it only injects the `http-equiv` ` ` tag for you. In case you are a fan of CSP, you must know that [Observatory](https://observatory.mozilla.org) does not support CSP in meta tags for now. Anyway, setting CSP in HTTP headers is better since there are some tags (e.g. `frame-ancestors` could not be set in ` `.",
        "On the other hand, (oh by the way this site is hosted with `Netlify`), there is a Gatsby plugin for generating some Netlify-specific \"server-side\" config scripts, namely `_headers` and `_redirects` :",
        "**[gatsby-plugin-netlify](https://www.gatsbyjs.org/packages/gatsby-plugin-netlify/)**",
        "Then I found that the `transformHeaders: (headers, path)` option seems to be configurable for my use case.",
        "1. the `gatsby-plugin-csp` generates the CSP hashes, build the CSP string and inject it as ` ` in *each* generated HTML file, in the `./public` directory by default. 2. the `gatsby-plugin-netlify` loops over each path, and the `transformHeaders` method is called upon processing of each page. You can manually update the `headers` argument according to the `path` argument. This is where the hack happens: - read each generated HTML file - extract the `content` value of the tag ` ` - update the variable `headers` with the CSP header - remove the CSP meta tag in the original HTML file",
        "That's all! No need to hack the original two plugins at all while you don't have to struggle writing a new Gatsby plugin from scratch.",
        "module.exports = { plugins: [ { resolve: `gatsby-plugin-csp`, options: { disableOnDev: false, reportOnly: false, mergeScriptHashes: true, mergeStyleHashes: true, mergeDefaultDirectives: true, }, }, { resolve: `gatsby-plugin-netlify`, options: { transformHeaders: (headers, path) => { if (path.endsWith('/')) { const filePath = `./public${path}index.html`; const rawHtml = readFileSync(filePath).toString(); const csp = / /.exec(rawHtml)[1].replace(/'/g, `'`); headers.push(`Content-Security-Policy: ${csp}`); writeFileSync(filePath, rawHtml.replace(/ /g, '')); } return headers; }, mergeSecurityHeaders: true, mergeLinkHeaders: true, mergeCachingHeaders: true, generateMatchPathRewrites: true, }, }, ], }; ```"
      ]
    },
    {
      "id": "article:domain-analyze-tools",
      "source_type": "article",
      "title": "Online Tools That Help You Analyze a Domain",
      "url": "https://gabrielkoo.com/blog/domain-analyze-tools/",
      "canonical_url": "https://gabrielkoo.com/blog/domain-analyze-tools/",
      "published_at": "2019-07-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "domain",
        "security",
        "tools"
      ],
      "description": "A list of tools that can help you analyze whether any information is leaked with your domain.",
      "content": "## [Google](https://www.google.com)\n\nIn case you don't know, the you can use a search string like `site:domain.com.tld` to look at the pages that are indexed by Google.\n\n![Google Search for the Site somedomain.com](/assets/img/google-site-somedomain-com.png \"Google Search for the Site somedomain.com\")\n\n## [WHOIS](https://www.whois.com/whois)\n\nBasically it is a tool for you to check about the **owner** of a domain. Sometimes when people don't protect their own information well (e.g. they didn't purchase the privacy option when they buy the domain), you might even have their home address or phone number shown on the ICANN record.\n<https://www.whois.com/whois/somedomain.com>\n\n## [What's my DNS](https://www.whatsmydns.net)\n\nYou can know about information like:\n\n* DNS Provider (`NS`)\n* Hosting provider of the servers / websites (`A` / `AAAA` / `CNAME`)\n* Mail servers (`MX`)\n* Any other third-party tool the owner is using, e.g. `TXT` records for Google site verification, search console, etc\n\n![Result for somedomain.com on DNS Checker](/assets/img/dns-checker-somedomain-com.png \"Result for somedomain.com on DNS Checker\")",
      "excerpts": [
        "[Google](https://www.google.com)",
        "In case you don't know, the you can use a search string like `site:domain.com.tld` to look at the pages that are indexed by Google.",
        "![Google Search for the Site somedomain.com](/assets/img/google-site-somedomain-com.png \"Google Search for the Site somedomain.com\")",
        "[WHOIS](https://www.whois.com/whois)",
        "Basically it is a tool for you to check about the **owner** of a domain. Sometimes when people don't protect their own information well (e.g. they didn't purchase the privacy option when they buy the domain), you might even have their home address or phone number shown on the ICANN record.",
        "[What's my DNS](https://www.whatsmydns.net)",
        "You can know about information like:",
        "* DNS Provider (`NS`) * Hosting provider of the servers / websites (`A` / `AAAA` / `CNAME`) * Mail servers (`MX`) * Any other third-party tool the owner is using, e.g. `TXT` records for Google site verification, search console, etc",
        "![Result for somedomain.com on DNS Checker](/assets/img/dns-checker-somedomain-com.png \"Result for somedomain.com on DNS Checker\")"
      ]
    },
    {
      "id": "project:tavily-oauth-mcp-wrapper",
      "source_type": "project",
      "title": "tavily-oauth-mcp-wrapper",
      "url": "https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper",
      "canonical_url": "https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper",
      "published_at": "2026-06-07",
      "last_verified_at": "2026-08-23",
      "tags": [
        "python"
      ],
      "description": "Per-user OAuth 2.1 in front of a shared-key API — Cognito + API Gateway + Lambda MCP wrapper. Companion to AgentCon HK 2026.",
      "metadata": {
        "language": "Python",
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-06-07T02:47:44Z"
      },
      "content": "Per-user OAuth 2.1 in front of a shared-key API — Cognito + API Gateway + Lambda MCP wrapper. Companion to AgentCon HK 2026.\n\nLanguage: Python\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper",
      "excerpts": [
        "Per-user OAuth 2.1 in front of a shared-key API — Cognito + API Gateway + Lambda MCP wrapper. Companion to AgentCon HK 2026.",
        "Repository: https://github.com/gabrielkoo/tavily-oauth-mcp-wrapper"
      ]
    },
    {
      "id": "project:otlp-cloudwatch-proxy",
      "source_type": "project",
      "title": "otlp-cloudwatch-proxy",
      "url": "https://github.com/gabrielkoo/otlp-cloudwatch-proxy",
      "canonical_url": "https://github.com/gabrielkoo/otlp-cloudwatch-proxy",
      "published_at": "2026-04-18",
      "last_verified_at": "2026-08-23",
      "tags": [],
      "description": "Lambdaless OTLP proxy to Amazon CloudWatch. Routes OpenTelemetry telemetry through API Gateway REST API with SigV4 signing. No Lambda. No collector. No code.",
      "metadata": {
        "language": null,
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-04-18T12:44:49Z"
      },
      "content": "Lambdaless OTLP proxy to Amazon CloudWatch. Routes OpenTelemetry telemetry through API Gateway REST API with SigV4 signing. No Lambda. No collector. No code.\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/otlp-cloudwatch-proxy",
      "excerpts": [
        "Lambdaless OTLP proxy to Amazon CloudWatch. Routes OpenTelemetry telemetry through API Gateway REST API with SigV4 signing. No Lambda. No collector. No code.",
        "Repository: https://github.com/gabrielkoo/otlp-cloudwatch-proxy"
      ]
    },
    {
      "id": "project:agentcon-2026-hk-demo",
      "source_type": "project",
      "title": "agentcon-2026-hk-demo",
      "url": "https://github.com/gabrielkoo/agentcon-2026-hk-demo",
      "canonical_url": "https://github.com/gabrielkoo/agentcon-2026-hk-demo",
      "published_at": "2026-06-20",
      "last_verified_at": "2026-08-23",
      "tags": [],
      "description": "",
      "metadata": {
        "language": null,
        "stars": 2,
        "forks": 0,
        "updated_at": "2026-06-20T04:14:24Z"
      },
      "content": "\n\nGitHub stars: 2; forks: 0.\n\nRepository: https://github.com/gabrielkoo/agentcon-2026-hk-demo",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/agentcon-2026-hk-demo"
      ]
    },
    {
      "id": "project:aws-lambda-whisper-adaptor",
      "source_type": "project",
      "title": "aws-lambda-whisper-adaptor",
      "url": "https://github.com/gabrielkoo/aws-lambda-whisper-adaptor",
      "canonical_url": "https://github.com/gabrielkoo/aws-lambda-whisper-adaptor",
      "published_at": "2026-06-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "python"
      ],
      "description": "Deepgram & OpenAI compatible speech-to-text on AWS Lambda using faster-whisper",
      "metadata": {
        "language": "Python",
        "stars": 2,
        "forks": 0,
        "updated_at": "2026-06-27T13:31:41Z"
      },
      "content": "Deepgram & OpenAI compatible speech-to-text on AWS Lambda using faster-whisper\n\nLanguage: Python\n\nGitHub stars: 2; forks: 0.\n\nRepository: https://github.com/gabrielkoo/aws-lambda-whisper-adaptor",
      "excerpts": [
        "Deepgram & OpenAI compatible speech-to-text on AWS Lambda using faster-whisper",
        "Repository: https://github.com/gabrielkoo/aws-lambda-whisper-adaptor"
      ]
    },
    {
      "id": "project:devto",
      "source_type": "project",
      "title": "devto",
      "url": "https://github.com/gabrielkoo/devto",
      "canonical_url": "https://github.com/gabrielkoo/devto",
      "published_at": "2026-03-15",
      "last_verified_at": "2026-08-23",
      "tags": [
        "html"
      ],
      "description": "dev.to article analytics dashboard",
      "metadata": {
        "language": "HTML",
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-03-15T15:41:15Z"
      },
      "content": "dev.to article analytics dashboard\n\nLanguage: HTML\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/devto",
      "excerpts": [
        "dev.to article analytics dashboard",
        "Repository: https://github.com/gabrielkoo/devto"
      ]
    },
    {
      "id": "project:bedrock-web-search-proxy",
      "source_type": "project",
      "title": "bedrock-web-search-proxy",
      "url": "https://github.com/gabrielkoo/bedrock-web-search-proxy",
      "canonical_url": "https://github.com/gabrielkoo/bedrock-web-search-proxy",
      "published_at": "2026-03-14",
      "last_verified_at": "2026-08-23",
      "tags": [
        "python"
      ],
      "description": "Drop-in Perplexity Sonar API replacement backed by AWS Bedrock Nova web grounding",
      "metadata": {
        "language": "Python",
        "stars": 2,
        "forks": 0,
        "updated_at": "2026-03-14T15:35:08Z"
      },
      "content": "Drop-in Perplexity Sonar API replacement backed by AWS Bedrock Nova web grounding\n\nLanguage: Python\n\nGitHub stars: 2; forks: 0.\n\nRepository: https://github.com/gabrielkoo/bedrock-web-search-proxy",
      "excerpts": [
        "Drop-in Perplexity Sonar API replacement backed by AWS Bedrock Nova web grounding",
        "Repository: https://github.com/gabrielkoo/bedrock-web-search-proxy"
      ]
    },
    {
      "id": "project:test-official-amazon-bedrock-openai-compatible-endpoint",
      "source_type": "project",
      "title": "test-official-amazon-bedrock-openai-compatible-endpoint",
      "url": "https://github.com/gabrielkoo/test-official-amazon-bedrock-openai-compatible-endpoint",
      "canonical_url": "https://github.com/gabrielkoo/test-official-amazon-bedrock-openai-compatible-endpoint",
      "published_at": "2025-10-15",
      "last_verified_at": "2026-08-23",
      "tags": [
        "amazon-bedrock",
        "aws",
        "openai",
        "openai-api",
        "openai-sdk",
        "shell"
      ],
      "description": "Amazon Bedrock launched the official OpenAI compatible endpoint on 2025-08-05 - This repository contains a list of tests on the actual OpenAI features/parameters compatibility.",
      "metadata": {
        "language": "Shell",
        "stars": 1,
        "forks": 0,
        "updated_at": "2025-10-15T14:43:00Z"
      },
      "content": "Amazon Bedrock launched the official OpenAI compatible endpoint on 2025-08-05 - This repository contains a list of tests on the actual OpenAI features/parameters compatibility.\n\nLanguage: Shell\n\nTopics: amazon-bedrock, aws, openai, openai-api, openai-sdk\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/test-official-amazon-bedrock-openai-compatible-endpoint",
      "excerpts": [
        "Amazon Bedrock launched the official OpenAI compatible endpoint on 2025-08-05 - This repository contains a list of tests on the actual OpenAI features/parameters compatibility.",
        "Topics: amazon-bedrock, aws, openai, openai-api, openai-sdk",
        "Repository: https://github.com/gabrielkoo/test-official-amazon-bedrock-openai-compatible-endpoint"
      ]
    },
    {
      "id": "project:devto-stats-github-action",
      "source_type": "project",
      "title": "devto-stats-github-action",
      "url": "https://github.com/gabrielkoo/devto-stats-github-action",
      "canonical_url": "https://github.com/gabrielkoo/devto-stats-github-action",
      "published_at": "2026-08-23",
      "last_verified_at": "2026-08-23",
      "tags": [
        "actions",
        "devto",
        "kiro-ide",
        "python"
      ],
      "description": "A GitHub Action that updates your Dev.to articles metrics daily into JSON data and SVG badges.",
      "metadata": {
        "language": "Python",
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-08-23T00:53:51Z"
      },
      "content": "A GitHub Action that updates your Dev.to articles metrics daily into JSON data and SVG badges.\n\nLanguage: Python\n\nTopics: actions, devto, kiro-ide\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/devto-stats-github-action",
      "excerpts": [
        "A GitHub Action that updates your Dev.to articles metrics daily into JSON data and SVG badges.",
        "Topics: actions, devto, kiro-ide",
        "Repository: https://github.com/gabrielkoo/devto-stats-github-action"
      ]
    },
    {
      "id": "project:q-dependabot",
      "source_type": "project",
      "title": "q-dependabot",
      "url": "https://github.com/gabrielkoo/q-dependabot",
      "canonical_url": "https://github.com/gabrielkoo/q-dependabot",
      "published_at": "2025-07-21",
      "last_verified_at": "2026-08-23",
      "tags": [
        "shell"
      ],
      "description": "A user-driven dependency monitoring solution powered by Amazon Q Developer CLI that generates intelligent package release digests and update recommendations for your projects.",
      "metadata": {
        "language": "Shell",
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-07-21T01:34:55Z"
      },
      "content": "A user-driven dependency monitoring solution powered by Amazon Q Developer CLI that generates intelligent package release digests and update recommendations for your projects.\n\nLanguage: Shell\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/q-dependabot",
      "excerpts": [
        "A user-driven dependency monitoring solution powered by Amazon Q Developer CLI that generates intelligent package release digests and update recommendations for your projects.",
        "Repository: https://github.com/gabrielkoo/q-dependabot"
      ]
    },
    {
      "id": "project:amazon-q-developer-cli-webui",
      "source_type": "project",
      "title": "amazon-q-developer-cli-webui",
      "url": "https://github.com/gabrielkoo/amazon-q-developer-cli-webui",
      "canonical_url": "https://github.com/gabrielkoo/amazon-q-developer-cli-webui",
      "published_at": "2026-02-22",
      "last_verified_at": "2026-08-23",
      "tags": [
        "javascript"
      ],
      "description": "There isn't an official UI for the Q CLI, so I vibe coded one.",
      "metadata": {
        "language": "JavaScript",
        "stars": 19,
        "forks": 5,
        "updated_at": "2026-02-22T23:35:21Z"
      },
      "content": "There isn't an official UI for the Q CLI, so I vibe coded one.\n\nLanguage: JavaScript\n\nGitHub stars: 19; forks: 5.\n\nRepository: https://github.com/gabrielkoo/amazon-q-developer-cli-webui",
      "excerpts": [
        "There isn't an official UI for the Q CLI, so I vibe coded one.",
        "Repository: https://github.com/gabrielkoo/amazon-q-developer-cli-webui"
      ]
    },
    {
      "id": "project:amazonq-plasma-sword-fighter-game",
      "source_type": "project",
      "title": "amazonq-plasma-sword-fighter-game",
      "url": "https://github.com/gabrielkoo/amazonq-plasma-sword-fighter-game",
      "canonical_url": "https://github.com/gabrielkoo/amazonq-plasma-sword-fighter-game",
      "published_at": "2025-06-19",
      "last_verified_at": "2026-08-23",
      "tags": [
        "amazon-q-cli",
        "amazon-q-developer",
        "genai",
        "python"
      ],
      "description": "Generate purely by Amazon Q Developer CLI.",
      "metadata": {
        "language": "Python",
        "stars": 1,
        "forks": 0,
        "updated_at": "2025-06-19T03:09:38Z"
      },
      "content": "Generate purely by Amazon Q Developer CLI.\n\nLanguage: Python\n\nTopics: amazon-q-cli, amazon-q-developer, genai\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/amazonq-plasma-sword-fighter-game",
      "excerpts": [
        "Generate purely by Amazon Q Developer CLI.",
        "Topics: amazon-q-cli, amazon-q-developer, genai",
        "Repository: https://github.com/gabrielkoo/amazonq-plasma-sword-fighter-game"
      ]
    },
    {
      "id": "project:gabrielkoo",
      "source_type": "project",
      "title": "gabrielkoo",
      "url": "https://github.com/gabrielkoo/gabrielkoo",
      "canonical_url": "https://github.com/gabrielkoo/gabrielkoo",
      "published_at": "2026-08-22",
      "last_verified_at": "2026-08-23",
      "tags": [],
      "description": "",
      "metadata": {
        "language": null,
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-08-22T02:47:19Z"
      },
      "content": "\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/gabrielkoo",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/gabrielkoo"
      ]
    },
    {
      "id": "project:aws-systems-manager-runbook-for-security",
      "source_type": "project",
      "title": "aws-systems-manager-runbook-for-security",
      "url": "https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security",
      "canonical_url": "https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security",
      "published_at": "2025-05-03",
      "last_verified_at": "2026-08-23",
      "tags": [],
      "description": "For AWS Summit Hong Kong 2025 Dev Chat - Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations",
      "metadata": {
        "language": null,
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-05-03T07:52:09Z"
      },
      "content": "For AWS Summit Hong Kong 2025 Dev Chat - Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security",
      "excerpts": [
        "For AWS Summit Hong Kong 2025 Dev Chat - Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations",
        "Repository: https://github.com/gabrielkoo/aws-systems-manager-runbook-for-security"
      ]
    },
    {
      "id": "project:aws-route53-dns-firewall-logging-cfn",
      "source_type": "project",
      "title": "aws-route53-dns-firewall-logging-cfn",
      "url": "https://github.com/gabrielkoo/aws-route53-dns-firewall-logging-cfn",
      "canonical_url": "https://github.com/gabrielkoo/aws-route53-dns-firewall-logging-cfn",
      "published_at": "2025-04-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "dns",
        "route53-resolver",
        "route53-resolver-firewall",
        "security"
      ],
      "description": "Protect any AWS VPC from malicious domains and capture every DNS query for observability—all in one drop-in stack.",
      "metadata": {
        "language": null,
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-04-27T04:34:44Z"
      },
      "content": "Protect any AWS VPC from malicious domains and capture every DNS query for observability—all in one drop-in stack.\n\nTopics: aws, dns, route53-resolver, route53-resolver-firewall, security\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/aws-route53-dns-firewall-logging-cfn",
      "excerpts": [
        "Protect any AWS VPC from malicious domains and capture every DNS query for observability—all in one drop-in stack.",
        "Topics: aws, dns, route53-resolver, route53-resolver-firewall, security",
        "Repository: https://github.com/gabrielkoo/aws-route53-dns-firewall-logging-cfn"
      ]
    },
    {
      "id": "project:tailscale-config-for-ai-services",
      "source_type": "project",
      "title": "tailscale-config-for-ai-services",
      "url": "https://github.com/gabrielkoo/tailscale-config-for-ai-services",
      "canonical_url": "https://github.com/gabrielkoo/tailscale-config-for-ai-services",
      "published_at": "2026-08-10",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anthropic",
        "apple-intelligence",
        "bedrock",
        "chatgpt",
        "claude",
        "generative-ai",
        "google-gemini",
        "notebooklm",
        "openai",
        "proxy",
        "tailscale",
        "javascript"
      ],
      "description": "About Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with Tailscale with an app connector, from an unrestricted region.",
      "metadata": {
        "language": "JavaScript",
        "stars": 16,
        "forks": 1,
        "updated_at": "2026-08-10T17:21:32Z"
      },
      "content": "About Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with Tailscale with an app connector, from an unrestricted region.\n\nLanguage: JavaScript\n\nTopics: anthropic, apple-intelligence, bedrock, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, proxy, tailscale\n\nGitHub stars: 16; forks: 1.\n\nRepository: https://github.com/gabrielkoo/tailscale-config-for-ai-services",
      "excerpts": [
        "About Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with Tailscale with an app connector, from an unrestricted region.",
        "Topics: anthropic, apple-intelligence, bedrock, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, proxy, tailscale",
        "Repository: https://github.com/gabrielkoo/tailscale-config-for-ai-services"
      ]
    },
    {
      "id": "project:twitter-post-metric-badge",
      "source_type": "project",
      "title": "twitter-post-metric-badge",
      "url": "https://github.com/gabrielkoo/twitter-post-metric-badge",
      "canonical_url": "https://github.com/gabrielkoo/twitter-post-metric-badge",
      "published_at": "2025-02-24",
      "last_verified_at": "2026-08-23",
      "tags": [
        "python"
      ],
      "description": "API to Render SVG Badge for X (Twitter) Post Metric, hosted on AWS SAM",
      "metadata": {
        "language": "Python",
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-02-24T02:25:51Z"
      },
      "content": "API to Render SVG Badge for X (Twitter) Post Metric, hosted on AWS SAM\n\nLanguage: Python\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/twitter-post-metric-badge",
      "excerpts": [
        "API to Render SVG Badge for X (Twitter) Post Metric, hosted on AWS SAM",
        "Repository: https://github.com/gabrielkoo/twitter-post-metric-badge"
      ]
    },
    {
      "id": "project:scalable-stateful-streamlit-chatbot-on-aws",
      "source_type": "project",
      "title": "scalable-stateful-streamlit-chatbot-on-aws",
      "url": "https://github.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws",
      "canonical_url": "https://github.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws",
      "published_at": "2025-04-16",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "bedrock",
        "chatbot",
        "docker",
        "ecs",
        "efs",
        "fargate",
        "genai",
        "python",
        "scalable",
        "stateful",
        "streamlit"
      ],
      "description": "Streamlit Chatbot build on AWS that is Truly Scalable Stateful",
      "metadata": {
        "language": "Python",
        "stars": 1,
        "forks": 1,
        "updated_at": "2025-04-16T20:21:49Z"
      },
      "content": "Streamlit Chatbot build on AWS that is Truly Scalable Stateful\n\nLanguage: Python\n\nTopics: aws, bedrock, chatbot, docker, ecs, efs, fargate, genai, python, scalable, stateful, streamlit\n\nGitHub stars: 1; forks: 1.\n\nRepository: https://github.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws",
      "excerpts": [
        "Streamlit Chatbot build on AWS that is Truly Scalable Stateful",
        "Topics: aws, bedrock, chatbot, docker, ecs, efs, fargate, genai, python, scalable, stateful, streamlit",
        "Repository: https://github.com/gabrielkoo/scalable-stateful-streamlit-chatbot-on-aws"
      ]
    },
    {
      "id": "project:bedrock-access-gateway-function-url",
      "source_type": "project",
      "title": "bedrock-access-gateway-function-url",
      "url": "https://github.com/gabrielkoo/bedrock-access-gateway-function-url",
      "canonical_url": "https://github.com/gabrielkoo/bedrock-access-gateway-function-url",
      "published_at": "2026-06-22",
      "last_verified_at": "2026-08-23",
      "tags": [
        "bedrock",
        "genai",
        "openai",
        "openai-api",
        "openai-proxy",
        "pay-as-you-go",
        "proxy",
        "sam",
        "serverless",
        "shell"
      ],
      "description": "OpenAI-Compatible RESTful APIs for Amazon Bedrock, modified from the original \"bedrock-access-gateway\" project for not using ALB, so that one could deploy and use it under a pay as you go model WITH NO FIXED COSTS.",
      "metadata": {
        "language": "Shell",
        "stars": 19,
        "forks": 4,
        "updated_at": "2026-06-22T11:57:07Z"
      },
      "content": "OpenAI-Compatible RESTful APIs for Amazon Bedrock, modified from the original \"bedrock-access-gateway\" project for not using ALB, so that one could deploy and use it under a pay as you go model WITH NO FIXED COSTS.\n\nLanguage: Shell\n\nTopics: bedrock, genai, openai, openai-api, openai-proxy, pay-as-you-go, proxy, sam, serverless\n\nGitHub stars: 19; forks: 4.\n\nRepository: https://github.com/gabrielkoo/bedrock-access-gateway-function-url",
      "excerpts": [
        "OpenAI-Compatible RESTful APIs for Amazon Bedrock, modified from the original \"bedrock-access-gateway\" project for not using ALB, so that one could deploy and use it under a pay as you go model WITH NO FIXED COSTS.",
        "Topics: bedrock, genai, openai, openai-api, openai-proxy, pay-as-you-go, proxy, sam, serverless",
        "Repository: https://github.com/gabrielkoo/bedrock-access-gateway-function-url"
      ]
    },
    {
      "id": "project:github-skyline",
      "source_type": "project",
      "title": "github-skyline",
      "url": "https://github.com/gabrielkoo/github-skyline",
      "canonical_url": "https://github.com/gabrielkoo/github-skyline",
      "published_at": "2024-12-09",
      "last_verified_at": "2026-08-23",
      "tags": [],
      "description": "",
      "metadata": {
        "language": null,
        "stars": 0,
        "forks": 0,
        "updated_at": "2024-12-09T16:37:54Z"
      },
      "content": "\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/github-skyline",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/github-skyline"
      ]
    },
    {
      "id": "project:wireguard-configs-for-ai-services",
      "source_type": "project",
      "title": "wireguard-configs-for-ai-services",
      "url": "https://github.com/gabrielkoo/wireguard-configs-for-ai-services",
      "canonical_url": "https://github.com/gabrielkoo/wireguard-configs-for-ai-services",
      "published_at": "2026-08-19",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anthropic",
        "apple-intelligence",
        "chatgpt",
        "claude",
        "generative-ai",
        "google-gemini",
        "notebooklm",
        "openai",
        "proxy",
        "split-tunnel",
        "wireguard",
        "python"
      ],
      "description": "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with WireGuard include tunneling, from an unrestricted region.",
      "metadata": {
        "language": "Python",
        "stars": 9,
        "forks": 2,
        "updated_at": "2026-08-19T15:28:25Z"
      },
      "content": "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with WireGuard include tunneling, from an unrestricted region.\n\nLanguage: Python\n\nTopics: anthropic, apple-intelligence, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, proxy, split-tunnel, wireguard\n\nGitHub stars: 9; forks: 2.\n\nRepository: https://github.com/gabrielkoo/wireguard-configs-for-ai-services",
      "excerpts": [
        "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with WireGuard include tunneling, from an unrestricted region.",
        "Topics: anthropic, apple-intelligence, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, proxy, split-tunnel, wireguard",
        "Repository: https://github.com/gabrielkoo/wireguard-configs-for-ai-services"
      ]
    },
    {
      "id": "project:openvpn-configs-for-ai-services",
      "source_type": "project",
      "title": "openvpn-configs-for-ai-services",
      "url": "https://github.com/gabrielkoo/openvpn-configs-for-ai-services",
      "canonical_url": "https://github.com/gabrielkoo/openvpn-configs-for-ai-services",
      "published_at": "2026-08-09",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anthropic",
        "apple-intelligence",
        "chatgpt",
        "claude",
        "generative-ai",
        "google-gemini",
        "notebooklm",
        "openai",
        "openvpn",
        "ovpn",
        "proxy",
        "split-tunnel",
        "python"
      ],
      "description": "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with OpenVPN include tunneling, from an unrestricted region.",
      "metadata": {
        "language": "Python",
        "stars": 4,
        "forks": 2,
        "updated_at": "2026-08-09T17:18:48Z"
      },
      "content": "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with OpenVPN include tunneling, from an unrestricted region.\n\nLanguage: Python\n\nTopics: anthropic, apple-intelligence, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, openvpn, ovpn, proxy, split-tunnel\n\nGitHub stars: 4; forks: 2.\n\nRepository: https://github.com/gabrielkoo/openvpn-configs-for-ai-services",
      "excerpts": [
        "Using OpenAI ChatGPT / Google Gemini / Anthropic Claude / Amazon Bedrock with OpenVPN include tunneling, from an unrestricted region.",
        "Topics: anthropic, apple-intelligence, chatgpt, claude, generative-ai, google-gemini, notebooklm, openai, openvpn, ovpn, proxy, split-tunnel",
        "Repository: https://github.com/gabrielkoo/openvpn-configs-for-ai-services"
      ]
    },
    {
      "id": "project:insta-post-link-extractor",
      "source_type": "project",
      "title": "insta-post-link-extractor",
      "url": "https://github.com/gabrielkoo/insta-post-link-extractor",
      "canonical_url": "https://github.com/gabrielkoo/insta-post-link-extractor",
      "published_at": "2024-11-16",
      "last_verified_at": "2026-08-23",
      "tags": [
        "automation",
        "aws-sam",
        "instagram",
        "shortcuts",
        "python"
      ],
      "description": "Save 10s on opening links in an Instagram post description, which are originally non-clickable.",
      "metadata": {
        "language": "Python",
        "stars": 1,
        "forks": 0,
        "updated_at": "2024-11-16T02:36:07Z"
      },
      "content": "Save 10s on opening links in an Instagram post description, which are originally non-clickable.\n\nLanguage: Python\n\nTopics: automation, aws-sam, instagram, shortcuts\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/insta-post-link-extractor",
      "excerpts": [
        "Save 10s on opening links in an Instagram post description, which are originally non-clickable.",
        "Topics: automation, aws-sam, instagram, shortcuts",
        "Repository: https://github.com/gabrielkoo/insta-post-link-extractor"
      ]
    },
    {
      "id": "project:aws-iam-identity-center-shortcut-portal",
      "source_type": "project",
      "title": "aws-iam-identity-center-shortcut-portal",
      "url": "https://github.com/gabrielkoo/aws-iam-identity-center-shortcut-portal",
      "canonical_url": "https://github.com/gabrielkoo/aws-iam-identity-center-shortcut-portal",
      "published_at": "2024-10-28",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws-identity-center",
        "aws-sso",
        "iam-role",
        "shortcuts",
        "javascript"
      ],
      "description": "A web-based interface for managing shortcuts to AWS IAM Identity Center access roles.",
      "metadata": {
        "language": "JavaScript",
        "stars": 1,
        "forks": 0,
        "updated_at": "2024-10-28T04:45:14Z"
      },
      "content": "A web-based interface for managing shortcuts to AWS IAM Identity Center access roles.\n\nLanguage: JavaScript\n\nTopics: aws-identity-center, aws-sso, iam-role, shortcuts\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/aws-iam-identity-center-shortcut-portal",
      "excerpts": [
        "A web-based interface for managing shortcuts to AWS IAM Identity Center access roles.",
        "Topics: aws-identity-center, aws-sso, iam-role, shortcuts",
        "Repository: https://github.com/gabrielkoo/aws-iam-identity-center-shortcut-portal"
      ]
    },
    {
      "id": "project:self-learning-rag-it-support-slackbot",
      "source_type": "project",
      "title": "self-learning-rag-it-support-slackbot",
      "url": "https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot",
      "canonical_url": "https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot",
      "published_at": "2025-01-10",
      "last_verified_at": "2026-08-23",
      "tags": [
        "ai-agent",
        "aurora-serverless",
        "bedrock",
        "claude",
        "rag",
        "serverless",
        "python"
      ],
      "description": "IT Support Slack Bot that self-learns and have access to knowledge base, with search and browsing tools.",
      "metadata": {
        "language": "Python",
        "stars": 2,
        "forks": 1,
        "updated_at": "2025-01-10T05:09:11Z"
      },
      "content": "IT Support Slack Bot that self-learns and have access to knowledge base, with search and browsing tools.\n\nLanguage: Python\n\nTopics: ai-agent, aurora-serverless, bedrock, claude, rag, serverless\n\nGitHub stars: 2; forks: 1.\n\nRepository: https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot",
      "excerpts": [
        "IT Support Slack Bot that self-learns and have access to knowledge base, with search and browsing tools.",
        "Topics: ai-agent, aurora-serverless, bedrock, claude, rag, serverless",
        "Repository: https://github.com/gabrielkoo/self-learning-rag-it-support-slackbot"
      ]
    },
    {
      "id": "project:hong-kong-public-holiday-api",
      "source_type": "project",
      "title": "hong-kong-public-holiday-api",
      "url": "https://github.com/gabrielkoo/hong-kong-public-holiday-api",
      "canonical_url": "https://github.com/gabrielkoo/hong-kong-public-holiday-api",
      "published_at": "2025-01-05",
      "last_verified_at": "2026-08-23",
      "tags": [
        "python"
      ],
      "description": "API for Hong Kong's public holiday, updated automatically from 1823's API.",
      "metadata": {
        "language": "Python",
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-01-05T10:37:29Z"
      },
      "content": "API for Hong Kong's public holiday, updated automatically from 1823's API.\n\nLanguage: Python\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/hong-kong-public-holiday-api",
      "excerpts": [
        "API for Hong Kong's public holiday, updated automatically from 1823's API.",
        "Repository: https://github.com/gabrielkoo/hong-kong-public-holiday-api"
      ]
    },
    {
      "id": "project:safeaws-cli",
      "source_type": "project",
      "title": "safeaws-cli",
      "url": "https://github.com/gabrielkoo/safeaws-cli",
      "canonical_url": "https://github.com/gabrielkoo/safeaws-cli",
      "published_at": "2024-11-28",
      "last_verified_at": "2026-08-23",
      "tags": [
        "amazon-bedrock",
        "aws",
        "aws-cli",
        "awscli",
        "awscliv2",
        "bedrock",
        "claude",
        "genai",
        "llm",
        "python"
      ],
      "description": "“Safe-W-S”, wrapper for AWS CLI that locates issues before executing",
      "metadata": {
        "language": "Python",
        "stars": 1,
        "forks": 0,
        "updated_at": "2024-11-28T06:06:56Z"
      },
      "content": "“Safe-W-S”, wrapper for AWS CLI that locates issues before executing\n\nLanguage: Python\n\nTopics: amazon-bedrock, aws, aws-cli, awscli, awscliv2, bedrock, claude, genai, llm\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/safeaws-cli",
      "excerpts": [
        "“Safe-W-S”, wrapper for AWS CLI that locates issues before executing",
        "Topics: amazon-bedrock, aws, aws-cli, awscli, awscliv2, bedrock, claude, genai, llm",
        "Repository: https://github.com/gabrielkoo/safeaws-cli"
      ]
    },
    {
      "id": "project:usagi",
      "source_type": "project",
      "title": "usagi",
      "url": "https://github.com/gabrielkoo/usagi",
      "canonical_url": "https://github.com/gabrielkoo/usagi",
      "published_at": "2024-01-24",
      "last_verified_at": "2026-08-23",
      "tags": [
        "html"
      ],
      "description": "That Usagi from Chiikawa",
      "metadata": {
        "language": "HTML",
        "stars": 0,
        "forks": 0,
        "updated_at": "2024-01-24T13:00:54Z"
      },
      "content": "That Usagi from Chiikawa\n\nLanguage: HTML\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/usagi",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/usagi"
      ]
    },
    {
      "id": "project:chatgpt-faq-slack-bot",
      "source_type": "project",
      "title": "chatgpt-faq-slack-bot",
      "url": "https://github.com/gabrielkoo/chatgpt-faq-slack-bot",
      "canonical_url": "https://github.com/gabrielkoo/chatgpt-faq-slack-bot",
      "published_at": "2025-04-14",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "aws-lambda",
        "chatbot",
        "chatgpt",
        "embedding",
        "faq",
        "kbqa",
        "knowledge-base",
        "openai",
        "qna",
        "question-answering",
        "retrieval-augmented-generation",
        "sam",
        "serverless",
        "slack",
        "slack-bot",
        "slackbot",
        "python"
      ],
      "description": "A user-trainable Knowledge Base / FAQ Slack Bot on AWS SAM based on ChatGPT and Embeddings.",
      "metadata": {
        "language": "Python",
        "stars": 21,
        "forks": 4,
        "updated_at": "2025-04-14T13:23:55Z"
      },
      "content": "A user-trainable Knowledge Base / FAQ Slack Bot on AWS SAM based on ChatGPT and Embeddings.\n\nLanguage: Python\n\nTopics: aws, aws-lambda, chatbot, chatgpt, embedding, faq, kbqa, knowledge-base, openai, qna, question-answering, retrieval-augmented-generation, sam, serverless, slack, slack-bot, slackbot\n\nGitHub stars: 21; forks: 4.\n\nRepository: https://github.com/gabrielkoo/chatgpt-faq-slack-bot",
      "excerpts": [
        "A user-trainable Knowledge Base / FAQ Slack Bot on AWS SAM based on ChatGPT and Embeddings.",
        "Topics: aws, aws-lambda, chatbot, chatgpt, embedding, faq, kbqa, knowledge-base, openai, qna, question-answering, retrieval-augmented-generation, sam, serverless, slack, slack-bot, slackbot",
        "Repository: https://github.com/gabrielkoo/chatgpt-faq-slack-bot"
      ]
    },
    {
      "id": "project:eud-to-hkd-wo-tax",
      "source_type": "project",
      "title": "eud-to-hkd-wo-tax",
      "url": "https://github.com/gabrielkoo/eud-to-hkd-wo-tax",
      "canonical_url": "https://github.com/gabrielkoo/eud-to-hkd-wo-tax",
      "published_at": "2023-04-03",
      "last_verified_at": "2026-08-23",
      "tags": [
        "html"
      ],
      "description": "Generated by ChatGPT",
      "metadata": {
        "language": "HTML",
        "stars": 0,
        "forks": 1,
        "updated_at": "2023-04-03T14:05:35Z"
      },
      "content": "Generated by ChatGPT\n\nLanguage: HTML\n\nGitHub stars: 0; forks: 1.\n\nRepository: https://github.com/gabrielkoo/eud-to-hkd-wo-tax",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/eud-to-hkd-wo-tax"
      ]
    },
    {
      "id": "project:aws-console-switch-role-portal",
      "source_type": "project",
      "title": "aws-console-switch-role-portal",
      "url": "https://github.com/gabrielkoo/aws-console-switch-role-portal",
      "canonical_url": "https://github.com/gabrielkoo/aws-console-switch-role-portal",
      "published_at": "2025-02-01",
      "last_verified_at": "2026-08-23",
      "tags": [
        "assume-role",
        "aws",
        "aws-console",
        "switch-role",
        "typescript"
      ],
      "description": "A portal for AWS role switching, for security-aware professionals.",
      "metadata": {
        "language": "TypeScript",
        "stars": 0,
        "forks": 0,
        "updated_at": "2025-02-01T01:13:37Z"
      },
      "content": "A portal for AWS role switching, for security-aware professionals.\n\nLanguage: TypeScript\n\nTopics: assume-role, aws, aws-console, switch-role\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/aws-console-switch-role-portal",
      "excerpts": [
        "A portal for AWS role switching, for security-aware professionals.",
        "Topics: assume-role, aws, aws-console, switch-role",
        "Repository: https://github.com/gabrielkoo/aws-console-switch-role-portal"
      ]
    },
    {
      "id": "project:json-ld-breadcrumb",
      "source_type": "project",
      "title": "json-ld-breadcrumb",
      "url": "https://github.com/gabrielkoo/json-ld-breadcrumb",
      "canonical_url": "https://github.com/gabrielkoo/json-ld-breadcrumb",
      "published_at": "2021-02-05",
      "last_verified_at": "2026-08-23",
      "tags": [
        "breadcrumb",
        "google-search",
        "json-ld",
        "schema-org",
        "seo",
        "structured-data",
        "javascript"
      ],
      "description": "Generate breadcrumb in JSON-LD format and inject into HTML (P.S. Random afternoon tea project)",
      "metadata": {
        "language": "JavaScript",
        "stars": 1,
        "forks": 0,
        "updated_at": "2021-02-05T17:31:57Z"
      },
      "content": "Generate breadcrumb in JSON-LD format and inject into HTML (P.S. Random afternoon tea project)\n\nLanguage: JavaScript\n\nTopics: breadcrumb, google-search, json-ld, schema-org, seo, structured-data\n\nGitHub stars: 1; forks: 0.\n\nRepository: https://github.com/gabrielkoo/json-ld-breadcrumb",
      "excerpts": [
        "Generate breadcrumb in JSON-LD format and inject into HTML (P.S. Random afternoon tea project)",
        "Topics: breadcrumb, google-search, json-ld, schema-org, seo, structured-data",
        "Repository: https://github.com/gabrielkoo/json-ld-breadcrumb"
      ]
    },
    {
      "id": "project:aws-systemmanager-automation-roles",
      "source_type": "project",
      "title": "aws-systemmanager-automation-roles",
      "url": "https://github.com/gabrielkoo/aws-systemmanager-automation-roles",
      "canonical_url": "https://github.com/gabrielkoo/aws-systemmanager-automation-roles",
      "published_at": "2021-02-02",
      "last_verified_at": "2026-08-23",
      "tags": [
        "aws",
        "aws-automation",
        "aws-config",
        "aws-config-rules",
        "aws-iam",
        "aws-role",
        "aws-systemmanager",
        "devops",
        "iam",
        "python"
      ],
      "description": "Generate IAM Roles for AWS System Manager Automation Documents",
      "metadata": {
        "language": "Python",
        "stars": 1,
        "forks": 2,
        "updated_at": "2021-02-02T23:09:51Z"
      },
      "content": "Generate IAM Roles for AWS System Manager Automation Documents\n\nLanguage: Python\n\nTopics: aws, aws-automation, aws-config, aws-config-rules, aws-iam, aws-role, aws-systemmanager, devops, iam\n\nGitHub stars: 1; forks: 2.\n\nRepository: https://github.com/gabrielkoo/aws-systemmanager-automation-roles",
      "excerpts": [
        "Generate IAM Roles for AWS System Manager Automation Documents",
        "Topics: aws, aws-automation, aws-config, aws-config-rules, aws-iam, aws-role, aws-systemmanager, devops, iam",
        "Repository: https://github.com/gabrielkoo/aws-systemmanager-automation-roles"
      ]
    },
    {
      "id": "project:gabrielkoo.github.io",
      "source_type": "project",
      "title": "gabrielkoo.github.io",
      "url": "https://github.com/gabrielkoo/gabrielkoo.github.io",
      "canonical_url": "https://github.com/gabrielkoo/gabrielkoo.github.io",
      "published_at": "2026-03-07",
      "last_verified_at": "2026-08-23",
      "tags": [
        "html"
      ],
      "description": "",
      "metadata": {
        "language": "HTML",
        "stars": 0,
        "forks": 0,
        "updated_at": "2026-03-07T01:59:17Z"
      },
      "content": "\n\nLanguage: HTML\n\nGitHub stars: 0; forks: 0.\n\nRepository: https://github.com/gabrielkoo/gabrielkoo.github.io",
      "excerpts": [
        "Repository: https://github.com/gabrielkoo/gabrielkoo.github.io"
      ]
    },
    {
      "id": "talk:2026-04-11-empower-team-wide-vibe-coding-with-llm-gateway-and-security-first-mcps",
      "source_type": "talk",
      "title": "Empower Team Wide Vibe Coding with LLM Gateway and Security-First MCPs",
      "url": "https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/",
      "canonical_url": "https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/",
      "published_at": "2026-04-11",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "security",
        "ai"
      ],
      "description": "AgentCon Hong Kong — Empower Team Wide Vibe Coding with LLM Gateway and Security-First MCPs",
      "metadata": {
        "occasion": "AgentCon Hong Kong",
        "related_urls": [
          "https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/"
        ]
      },
      "content": "Empower Team Wide Vibe Coding with LLM Gateway and Security-First MCPs\n\nOccasion: AgentCon Hong Kong\n\nDate: 2026-04-11\n\nSources: https://the-quantum-nargle.github.io/agentcon-2026-hk-slides/",
      "excerpts": [
        "Empower Team Wide Vibe Coding with LLM Gateway and Security-First MCPs. AgentCon Hong Kong, 2026-04-11."
      ]
    },
    {
      "id": "talk:2025-11-18-panel-strategies-in-action-finance-fintech-success-stories",
      "source_type": "talk",
      "title": "Panel: Strategies in Action - Finance & Fintech Success Stories",
      "url": "https://www.linkedin.com/posts/angellampy_fintech-ai-googlecloud-activity-7396770455250063360-2X1K",
      "canonical_url": "https://www.linkedin.com/posts/angellampy_fintech-ai-googlecloud-activity-7396770455250063360-2X1K",
      "published_at": "2025-11-18",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "fintech"
      ],
      "description": "The Fintech Growth Playbook Google Hong Kong — Panel: Strategies in Action - Finance & Fintech Success Stories",
      "metadata": {
        "occasion": "The Fintech Growth Playbook Google Hong Kong",
        "related_urls": [
          "https://www.linkedin.com/posts/angellampy_fintech-ai-googlecloud-activity-7396770455250063360-2X1K"
        ]
      },
      "content": "Panel: Strategies in Action - Finance & Fintech Success Stories\n\nOccasion: The Fintech Growth Playbook Google Hong Kong\n\nDate: 2025-11-18\n\nSources: https://www.linkedin.com/posts/angellampy_fintech-ai-googlecloud-activity-7396770455250063360-2X1K",
      "excerpts": [
        "Panel: Strategies in Action - Finance & Fintech Success Stories. The Fintech Growth Playbook Google Hong Kong, 2025-11-18."
      ]
    },
    {
      "id": "talk:2025-11-03-panel-agentic-ai-in-banking-financial-services",
      "source_type": "talk",
      "title": "Panel: Agentic AI in Banking & Financial Services",
      "url": "https://aws-fintech-nextwave.splashthat.com/",
      "canonical_url": "https://aws-fintech-nextwave.splashthat.com/",
      "published_at": "2025-11-03",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "ai",
        "fintech"
      ],
      "description": "HK Fintech Week AWS Next Wave of Fintech — Panel: Agentic AI in Banking & Financial Services",
      "metadata": {
        "occasion": "HK Fintech Week AWS Next Wave of Fintech",
        "related_urls": [
          "https://aws-fintech-nextwave.splashthat.com/"
        ]
      },
      "content": "Panel: Agentic AI in Banking & Financial Services\n\nOccasion: HK Fintech Week AWS Next Wave of Fintech\n\nDate: 2025-11-03\n\nSources: https://aws-fintech-nextwave.splashthat.com/",
      "excerpts": [
        "Panel: Agentic AI in Banking & Financial Services. HK Fintech Week AWS Next Wave of Fintech, 2025-11-03."
      ]
    },
    {
      "id": "talk:2025-11-02-organizer",
      "source_type": "talk",
      "title": "Organizer",
      "url": "https://awscommunity.hk/",
      "canonical_url": "https://awscommunity.hk/",
      "published_at": "2025-11-02",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws"
      ],
      "description": "AWS Community Day HK — Organizer",
      "metadata": {
        "occasion": "AWS Community Day HK",
        "related_urls": [
          "https://awscommunity.hk/"
        ]
      },
      "content": "Organizer\n\nOccasion: AWS Community Day HK\n\nDate: 2025-11-02\n\nSources: https://awscommunity.hk/",
      "excerpts": [
        "Organizer. AWS Community Day HK, 2025-11-02."
      ]
    },
    {
      "id": "talk:2025-10-16-podcast-kiro-series-ep7-step-by-step-spec-driven-tutorial-building-your-customized-data-analytics-and-visualization-dashboard",
      "source_type": "talk",
      "title": "Podcast: Kiro Series Ep7 - Step-by-Step Spec-Driven Tutorial: Building your Customized Data Analytics and Visualization Dashboard",
      "url": "https://aws.amazon.com/events/aws-innovate/hk/migrate-and-modernize/",
      "canonical_url": "https://aws.amazon.com/events/aws-innovate/hk/migrate-and-modernize/",
      "published_at": "2025-10-16",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "ai"
      ],
      "description": "AWS Innovate - Migrate and Modernize — Podcast: Kiro Series Ep7 - Step-by-Step Spec-Driven Tutorial: Building your Customized Data Analytics and Visualization Dashboard",
      "metadata": {
        "occasion": "AWS Innovate - Migrate and Modernize",
        "related_urls": [
          "https://aws.amazon.com/events/aws-innovate/hk/migrate-and-modernize/"
        ]
      },
      "content": "Podcast: Kiro Series Ep7 - Step-by-Step Spec-Driven Tutorial: Building your Customized Data Analytics and Visualization Dashboard\n\nOccasion: AWS Innovate - Migrate and Modernize\n\nDate: 2025-10-16\n\nSources: https://aws.amazon.com/events/aws-innovate/hk/migrate-and-modernize/",
      "excerpts": [
        "Podcast: Kiro Series Ep7 - Step-by-Step Spec-Driven Tutorial: Building your Customized Data Analytics and Visualization Dashboard. AWS Innovate - Migrate and Modernize, 2025-10-16."
      ]
    },
    {
      "id": "talk:2025-05-08-empower-devs-with-least-privilege-aws-systems-manager-automation-for-secure-self-served-operations",
      "source_type": "talk",
      "title": "Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations",
      "url": "https://www.linkedin.com/posts/gabrielkoo_awsug-awscb-awshk-activity-7325538001067888640-AFP6",
      "canonical_url": "https://www.linkedin.com/posts/gabrielkoo_awsug-awscb-awshk-activity-7325538001067888640-AFP6",
      "published_at": "2025-05-08",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "security",
        "devops"
      ],
      "description": "AWS Summit HK — Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations",
      "metadata": {
        "occasion": "AWS Summit HK",
        "related_urls": [
          "https://www.linkedin.com/posts/gabrielkoo_awsug-awscb-awshk-activity-7325538001067888640-AFP6"
        ]
      },
      "content": "Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations\n\nOccasion: AWS Summit HK\n\nDate: 2025-05-08\n\nSources: https://www.linkedin.com/posts/gabrielkoo_awsug-awscb-awshk-activity-7325538001067888640-AFP6",
      "excerpts": [
        "Empower Devs with Least Privilege: AWS Systems Manager Automation for Secure Self-Served Operations. AWS Summit HK, 2025-05-08."
      ]
    },
    {
      "id": "talk:2024-09-27-sharing-on-serverless-ai-it-support-again",
      "source_type": "talk",
      "title": "Sharing on Serverless AI IT Support (Again)",
      "url": "https://www.meetup.com/hong-kong-amazon-aws-user-group/events/303522204/",
      "canonical_url": "https://www.meetup.com/hong-kong-amazon-aws-user-group/events/303522204/",
      "published_at": "2024-09-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "ai"
      ],
      "description": "AWS User Group HK - Gen AI Night — Sharing on Serverless AI IT Support (Again)",
      "metadata": {
        "occasion": "AWS User Group HK - Gen AI Night",
        "related_urls": [
          "https://www.meetup.com/hong-kong-amazon-aws-user-group/events/303522204/"
        ]
      },
      "content": "Sharing on Serverless AI IT Support (Again)\n\nOccasion: AWS User Group HK - Gen AI Night\n\nDate: 2024-09-27\n\nSources: https://www.meetup.com/hong-kong-amazon-aws-user-group/events/303522204/",
      "excerpts": [
        "Sharing on Serverless AI IT Support (Again). AWS User Group HK - Gen AI Night, 2024-09-27."
      ]
    },
    {
      "id": "talk:2024-07-24-serverless-ai-it-support",
      "source_type": "talk",
      "title": "Serverless AI IT Support",
      "url": "https://aws.amazon.com/tw/events/taiwan/2024-aws-summit-taipei/",
      "canonical_url": "https://aws.amazon.com/tw/events/taiwan/2024-aws-summit-taipei/",
      "published_at": "2024-07-24",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "ai"
      ],
      "description": "AWS Summit TW — Serverless AI IT Support",
      "metadata": {
        "occasion": "AWS Summit TW",
        "related_urls": [
          "https://aws.amazon.com/tw/events/taiwan/2024-aws-summit-taipei/"
        ]
      },
      "content": "Serverless AI IT Support\n\nOccasion: AWS Summit TW\n\nDate: 2024-07-24\n\nSources: https://aws.amazon.com/tw/events/taiwan/2024-aws-summit-taipei/",
      "excerpts": [
        "Serverless AI IT Support. AWS Summit TW, 2024-07-24."
      ]
    },
    {
      "id": "talk:2024-06-28-panel-individual-career-sharing",
      "source_type": "talk",
      "title": "Panel & Individual Career Sharing",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:7213715521140137985/",
      "canonical_url": "https://www.linkedin.com/feed/update/urn:li:activity:7213715521140137985/",
      "published_at": "2024-06-28",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws"
      ],
      "description": "AWS User Group HK - Career Night — Panel & Individual Career Sharing",
      "metadata": {
        "occasion": "AWS User Group HK - Career Night",
        "related_urls": [
          "https://www.linkedin.com/feed/update/urn:li:activity:7213715521140137985/"
        ]
      },
      "content": "Panel & Individual Career Sharing\n\nOccasion: AWS User Group HK - Career Night\n\nDate: 2024-06-28\n\nSources: https://www.linkedin.com/feed/update/urn:li:activity:7213715521140137985/",
      "excerpts": [
        "Panel & Individual Career Sharing. AWS User Group HK - Career Night, 2024-06-28."
      ]
    },
    {
      "id": "talk:2023-05-23-building-a-knowledge-based-serverless-ai-powered-slackbot-on-aws",
      "source_type": "talk",
      "title": "Building a Knowledge Based Serverless AI powered Slackbot on AWS",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:7067158451709247488/",
      "canonical_url": "https://www.linkedin.com/feed/update/urn:li:activity:7067158451709247488/",
      "published_at": "2023-05-23",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "ai"
      ],
      "description": "AWS Summit HK — Building a Knowledge Based Serverless AI powered Slackbot on AWS",
      "metadata": {
        "occasion": "AWS Summit HK",
        "related_urls": [
          "https://www.linkedin.com/feed/update/urn:li:activity:7067158451709247488/"
        ]
      },
      "content": "Building a Knowledge Based Serverless AI powered Slackbot on AWS\n\nOccasion: AWS Summit HK\n\nDate: 2023-05-23\n\nSources: https://www.linkedin.com/feed/update/urn:li:activity:7067158451709247488/",
      "excerpts": [
        "Building a Knowledge Based Serverless AI powered Slackbot on AWS. AWS Summit HK, 2023-05-23."
      ]
    },
    {
      "id": "talk:2023-04-27-panel-discussion",
      "source_type": "talk",
      "title": "Panel Discussion",
      "url": "https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-panel-discussion-level-up-your-career-a-developers-guide-to-upskilling-in-the-fast-paced-tech-industry",
      "canonical_url": "https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-panel-discussion-level-up-your-career-a-developers-guide-to-upskilling-in-the-fast-paced-tech-industry",
      "published_at": "2023-04-27",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws"
      ],
      "description": "AWS Dev Day HK — Panel Discussion",
      "metadata": {
        "occasion": "AWS Dev Day HK",
        "related_urls": [
          "https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-panel-discussion-level-up-your-career-a-developers-guide-to-upskilling-in-the-fast-paced-tech-industry",
          "https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-dev-meetup-build-and-host-your-serverless-ai-integrated-slack-qa-chatbot-on-aws"
        ]
      },
      "content": "Panel Discussion\n\nOccasion: AWS Dev Day HK\n\nDate: 2023-04-27\n\nSources: https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-panel-discussion-level-up-your-career-a-developers-guide-to-upskilling-in-the-fast-paced-tech-industry, https://hktw-resources.awscloud.com/hong-kong-aws-dev-day-hong-kong-2023/hong-kong-dev-meetup-build-and-host-your-serverless-ai-integrated-slack-qa-chatbot-on-aws",
      "excerpts": [
        "Panel Discussion. AWS Dev Day HK, 2023-04-27."
      ]
    },
    {
      "id": "talk:2021-08-02-enterprise-level-insurance-operations-powered-by-devsecops",
      "source_type": "talk",
      "title": "Enterprise-level Insurance Operations Powered by DevSecOps",
      "url": "https://hktw-resources.awscloud.com/aws-industry-week-for-financial-services/enterprise-level-insurance-operations-powered-by-devsecops",
      "canonical_url": "https://hktw-resources.awscloud.com/aws-industry-week-for-financial-services/enterprise-level-insurance-operations-powered-by-devsecops",
      "published_at": "2021-08-02",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "security",
        "devops"
      ],
      "description": "AWS Industry Week — Enterprise-level Insurance Operations Powered by DevSecOps",
      "metadata": {
        "occasion": "AWS Industry Week",
        "related_urls": [
          "https://hktw-resources.awscloud.com/aws-industry-week-for-financial-services/enterprise-level-insurance-operations-powered-by-devsecops"
        ]
      },
      "content": "Enterprise-level Insurance Operations Powered by DevSecOps\n\nOccasion: AWS Industry Week\n\nDate: 2021-08-02\n\nSources: https://hktw-resources.awscloud.com/aws-industry-week-for-financial-services/enterprise-level-insurance-operations-powered-by-devsecops",
      "excerpts": [
        "Enterprise-level Insurance Operations Powered by DevSecOps. AWS Industry Week, 2021-08-02."
      ]
    },
    {
      "id": "talk:2020-11-26-privacyops-fintech-revolution-or-evolution-in-the-insurance-industry",
      "source_type": "talk",
      "title": "PrivacyOps @ FinTech – Revolution or Evolution in the Insurance Industry?",
      "url": "https://cftasia.org/blogs/events/privacyops-fintech-revolution-or-evolution-in-the-insurance-industry",
      "canonical_url": "https://cftasia.org/blogs/events/privacyops-fintech-revolution-or-evolution-in-the-insurance-industry",
      "published_at": "2020-11-26",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "security",
        "fintech"
      ],
      "description": "HK IFTA Zoom Webminar — PrivacyOps @ FinTech – Revolution or Evolution in the Insurance Industry?",
      "metadata": {
        "occasion": "HK IFTA Zoom Webminar",
        "related_urls": [
          "https://cftasia.org/blogs/events/privacyops-fintech-revolution-or-evolution-in-the-insurance-industry"
        ]
      },
      "content": "PrivacyOps @ FinTech – Revolution or Evolution in the Insurance Industry?\n\nOccasion: HK IFTA Zoom Webminar\n\nDate: 2020-11-26\n\nSources: https://cftasia.org/blogs/events/privacyops-fintech-revolution-or-evolution-in-the-insurance-industry",
      "excerpts": [
        "PrivacyOps @ FinTech – Revolution or Evolution in the Insurance Industry?. HK IFTA Zoom Webminar, 2020-11-26."
      ]
    },
    {
      "id": "talk:2020-01-13-utilizing-amazon-guardduty-to-automate-security-and-improve-visibility",
      "source_type": "talk",
      "title": "Utilizing Amazon GuardDuty to Automate Security and Improve Visibility",
      "url": "https://www.youtube.com/watch?v=o5TM9P8U6EY",
      "canonical_url": "https://www.youtube.com/watch?v=o5TM9P8U6EY",
      "published_at": "2020-01-13",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws",
        "security"
      ],
      "description": "\"This is My Architecture\" AWS Youtube Series — Utilizing Amazon GuardDuty to Automate Security and Improve Visibility",
      "metadata": {
        "occasion": "\"This is My Architecture\" AWS Youtube Series",
        "related_urls": [
          "https://www.youtube.com/watch?v=o5TM9P8U6EY"
        ]
      },
      "content": "Utilizing Amazon GuardDuty to Automate Security and Improve Visibility\n\nOccasion: \"This is My Architecture\" AWS Youtube Series\n\nDate: 2020-01-13\n\nSources: https://www.youtube.com/watch?v=o5TM9P8U6EY",
      "excerpts": [
        "Utilizing Amazon GuardDuty to Automate Security and Improve Visibility. \"This is My Architecture\" AWS Youtube Series, 2020-01-13."
      ]
    },
    {
      "id": "talk:2019-03-19-virtual-insurers-new-tools-for-a-new-world",
      "source_type": "talk",
      "title": "Virtual Insurers - New Tools for a New World",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:6525240503271809024/",
      "canonical_url": "https://www.linkedin.com/feed/update/urn:li:activity:6525240503271809024/",
      "published_at": "2019-03-19",
      "last_verified_at": "2026-08-23",
      "tags": [
        "talk",
        "aws"
      ],
      "description": "AWS Financial Symposium — Virtual Insurers - New Tools for a New World",
      "metadata": {
        "occasion": "AWS Financial Symposium",
        "related_urls": [
          "https://www.linkedin.com/feed/update/urn:li:activity:6525240503271809024/",
          "https://www.slideshare.net/slideshow/virtualinsurersnewtoolsforanewworld/154395593"
        ]
      },
      "content": "Virtual Insurers - New Tools for a New World\n\nOccasion: AWS Financial Symposium\n\nDate: 2019-03-19\n\nSources: https://www.linkedin.com/feed/update/urn:li:activity:6525240503271809024/, https://www.slideshare.net/slideshow/virtualinsurersnewtoolsforanewworld/154395593",
      "excerpts": [
        "Virtual Insurers - New Tools for a New World. AWS Financial Symposium, 2019-03-19."
      ]
    },
    {
      "id": "anime:2026-spring-rent-a-girlfriend-s5-367c9bf09b",
      "source_type": "anime",
      "title": "Rent-a-Girlfriend S5",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2026",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Rent-a-Girlfriend S5 — 2026 Spring",
      "metadata": {
        "year": 2026,
        "season": "Spring",
        "title_jp": "彼女、お借りします 第5期",
        "title_en": "Rent-a-Girlfriend S5",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rent-a-Girlfriend S5\nJapanese title: 彼女、お借りします 第5期\nYear: 2026\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rent-a-Girlfriend S5 — 2026 Spring."
      ]
    },
    {
      "id": "anime:2026-spring-re-zero-s4-306081c276",
      "source_type": "anime",
      "title": "Re:ZERO S4",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2026",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Re:ZERO S4 — 2026 Spring",
      "metadata": {
        "year": 2026,
        "season": "Spring",
        "title_jp": "Re:ゼロから始める異世界生活 4th",
        "title_en": "Re:ZERO S4",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:ZERO S4\nJapanese title: Re:ゼロから始める異世界生活 4th\nYear: 2026\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:ZERO S4 — 2026 Spring."
      ]
    },
    {
      "id": "anime:2026-winter-frieren-s2-4a09372561",
      "source_type": "anime",
      "title": "Frieren S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2026",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "favourite"
      ],
      "description": "Frieren S2 — 2026 Winter",
      "metadata": {
        "year": 2026,
        "season": "Winter",
        "title_jp": "葬送のフリーレン Season 2",
        "title_en": "Frieren S2",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Frieren S2\nJapanese title: 葬送のフリーレン Season 2\nYear: 2026\nSeason: Winter\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Frieren S2 — 2026 Winter — favourite."
      ]
    },
    {
      "id": "anime:2026-winter-oshi-no-ko-s3-6e60b1f215",
      "source_type": "anime",
      "title": "Oshi no Ko S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2026",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Oshi no Ko S3 — 2026 Winter",
      "metadata": {
        "year": 2026,
        "season": "Winter",
        "title_jp": "【推しの子】Season 3",
        "title_en": "Oshi no Ko S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Oshi no Ko S3\nJapanese title: 【推しの子】Season 3\nYear: 2026\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Oshi no Ko S3 — 2026 Winter."
      ]
    },
    {
      "id": "anime:2025-summer-ac49e1cd1f6b-a6095a16e8",
      "source_type": "anime",
      "title": "青春豬頭少年不會夢到聖誕服女郎",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2025",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "青春豬頭少年不會夢到聖誕服女郎 — 2025 Summer",
      "metadata": {
        "year": 2025,
        "season": "Summer",
        "title_jp": "青春ブタ野郎はサンタクロースの夢を見ない",
        "title_en": "青春豬頭少年不會夢到聖誕服女郎",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 青春豬頭少年不會夢到聖誕服女郎\nJapanese title: 青春ブタ野郎はサンタクロースの夢を見ない\nYear: 2025\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "青春豬頭少年不會夢到聖誕服女郎 — 2025 Summer."
      ]
    },
    {
      "id": "anime:2025-spring-gundam-gquuuuuux-e0722f502e",
      "source_type": "anime",
      "title": "機動戰士Gundam GQuuuuuuX",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2025",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "機動戰士Gundam GQuuuuuuX — 2025 Spring",
      "metadata": {
        "year": 2025,
        "season": "Spring",
        "title_jp": "機動戦士Gundam GQuuuuuuX",
        "title_en": "機動戰士Gundam GQuuuuuuX",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 機動戰士Gundam GQuuuuuuX\nJapanese title: 機動戦士Gundam GQuuuuuuX\nYear: 2025\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "機動戰士Gundam GQuuuuuuX — 2025 Spring."
      ]
    },
    {
      "id": "anime:2025-winter-bang-dream-ave-mujica-d202287eae",
      "source_type": "anime",
      "title": "BanG Dream! Ave Mujica",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2025",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "BanG Dream! Ave Mujica — 2025 Winter",
      "metadata": {
        "year": 2025,
        "season": "Winter",
        "title_jp": "BanG Dream! Ave Mujica",
        "title_en": "BanG Dream! Ave Mujica",
        "starred": false,
        "seichi": false
      },
      "content": "English title: BanG Dream! Ave Mujica\nJapanese title: BanG Dream! Ave Mujica\nYear: 2025\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "BanG Dream! Ave Mujica — 2025 Winter."
      ]
    },
    {
      "id": "anime:2025-winter-re-3rd-season-44affa9741",
      "source_type": "anime",
      "title": "Re:從零開始的異世界生活 3rd season 反擊篇",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2025",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Re:從零開始的異世界生活 3rd season 反擊篇 — 2025 Winter",
      "metadata": {
        "year": 2025,
        "season": "Winter",
        "title_jp": "Re:ゼロから始める異世界生活 3rd season 反擊編",
        "title_en": "Re:從零開始的異世界生活 3rd season 反擊篇",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:從零開始的異世界生活 3rd season 反擊篇\nJapanese title: Re:ゼロから始める異世界生活 3rd season 反擊編\nYear: 2025\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:從零開始的異世界生活 3rd season 反擊篇 — 2025 Winter."
      ]
    },
    {
      "id": "anime:2024-fall-re-zero-s3-99a1f17fed",
      "source_type": "anime",
      "title": "Re:ZERO S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Re:ZERO S3 — 2024 Fall",
      "metadata": {
        "year": 2024,
        "season": "Fall",
        "title_jp": "Re:ゼロから始める異世界生活 3rd",
        "title_en": "Re:ZERO S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:ZERO S3\nJapanese title: Re:ゼロから始める異世界生活 3rd\nYear: 2024\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:ZERO S3 — 2024 Fall."
      ]
    },
    {
      "id": "anime:2024-fall-gun-gale-online-ii-7154dc3e81",
      "source_type": "anime",
      "title": "刀劍神域外傳 Gun Gale Online II",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "刀劍神域外傳 Gun Gale Online II — 2024 Fall",
      "metadata": {
        "year": 2024,
        "season": "Fall",
        "title_jp": "ソードアート・オンライン オルタナティブ ガンゲイル・オンラインII",
        "title_en": "刀劍神域外傳 Gun Gale Online II",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 刀劍神域外傳 Gun Gale Online II\nJapanese title: ソードアート・オンライン オルタナティブ ガンゲイル・オンラインII\nYear: 2024\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域外傳 Gun Gale Online II — 2024 Fall."
      ]
    },
    {
      "id": "anime:2024-fall-love-live-superstar-eaab0d4be2",
      "source_type": "anime",
      "title": "Love Live! Superstar!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Love Live! Superstar!! — 2024 Fall",
      "metadata": {
        "year": 2024,
        "season": "Fall",
        "title_jp": "ラブライブ！スーパースター!!",
        "title_en": "Love Live! Superstar!!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Love Live! Superstar!!\nJapanese title: ラブライブ！スーパースター!!\nYear: 2024\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Love Live! Superstar!! — 2024 Fall."
      ]
    },
    {
      "id": "anime:2024-summer-oshi-no-ko-season-2-2af6160f19",
      "source_type": "anime",
      "title": "Oshi no Ko Season 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Oshi no Ko Season 2 — 2024 Summer",
      "metadata": {
        "year": 2024,
        "season": "Summer",
        "title_jp": "【推しの子】Season 2",
        "title_en": "Oshi no Ko Season 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Oshi no Ko Season 2\nJapanese title: 【推しの子】Season 2\nYear: 2024\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Oshi no Ko Season 2 — 2024 Summer."
      ]
    },
    {
      "id": "anime:2024-spring-sound-euphonium-s3-a839dbe5ed",
      "source_type": "anime",
      "title": "Sound! Euphonium S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "Sound! Euphonium S3 — 2024 Spring",
      "metadata": {
        "year": 2024,
        "season": "Spring",
        "title_jp": "響け！ユーフォニアム 第3期",
        "title_en": "Sound! Euphonium S3",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Sound! Euphonium S3\nJapanese title: 響け！ユーフォニアム 第3期\nYear: 2024\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Sound! Euphonium S3 — 2024 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2024-spring-konosuba-s3-69c50cea91",
      "source_type": "anime",
      "title": "KonoSuba S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "KonoSuba S3 — 2024 Spring",
      "metadata": {
        "year": 2024,
        "season": "Spring",
        "title_jp": "この素晴らしい世界に祝福を！3",
        "title_en": "KonoSuba S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: KonoSuba S3\nJapanese title: この素晴らしい世界に祝福を！3\nYear: 2024\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "KonoSuba S3 — 2024 Spring."
      ]
    },
    {
      "id": "anime:2024-spring-the-irregular-at-magic-high-school-s3-bac59b6fb7",
      "source_type": "anime",
      "title": "The Irregular at Magic High School S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "The Irregular at Magic High School S3 — 2024 Spring",
      "metadata": {
        "year": 2024,
        "season": "Spring",
        "title_jp": "魔法科高校の劣等生 3rd",
        "title_en": "The Irregular at Magic High School S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Irregular at Magic High School S3\nJapanese title: 魔法科高校の劣等生 3rd\nYear: 2024\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Irregular at Magic High School S3 — 2024 Spring."
      ]
    },
    {
      "id": "anime:2024-spring-date-a-live-v-395a26a89a",
      "source_type": "anime",
      "title": "Date A Live V",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2024",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Date A Live V — 2024 Spring",
      "metadata": {
        "year": 2024,
        "season": "Spring",
        "title_jp": "デート・ア・ライブⅤ",
        "title_en": "Date A Live V",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Date A Live V\nJapanese title: デート・ア・ライブⅤ\nYear: 2024\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Date A Live V — 2024 Spring."
      ]
    },
    {
      "id": "anime:2023-film-hibike-euphonium-ensemble-contest-8295989601",
      "source_type": "anime",
      "title": "Hibike! Euphonium: Ensemble Contest",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Hibike! Euphonium: Ensemble Contest — 2023 Film",
      "metadata": {
        "year": 2023,
        "season": "Film",
        "title_jp": "劇場版 響け！ユーフォニアム ~アンサンブルコンテスト~",
        "title_en": "Hibike! Euphonium: Ensemble Contest",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Hibike! Euphonium: Ensemble Contest\nJapanese title: 劇場版 響け！ユーフォニアム ~アンサンブルコンテスト~\nYear: 2023\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Hibike! Euphonium: Ensemble Contest — 2023 Film."
      ]
    },
    {
      "id": "anime:2023-film-rascal-does-not-dream-of-a-sister-venturing-out-2f434bb23a",
      "source_type": "anime",
      "title": "Rascal Does Not Dream of a Sister Venturing Out",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Rascal Does Not Dream of a Sister Venturing Out — 2023 Film",
      "metadata": {
        "year": 2023,
        "season": "Film",
        "title_jp": "青春ブタ野郎はおでかけシスターの夢を見ない",
        "title_en": "Rascal Does Not Dream of a Sister Venturing Out",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rascal Does Not Dream of a Sister Venturing Out\nJapanese title: 青春ブタ野郎はおでかけシスターの夢を見ない\nYear: 2023\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rascal Does Not Dream of a Sister Venturing Out — 2023 Film."
      ]
    },
    {
      "id": "anime:2023-fall-frieren-beyond-journey-s-end-f0f1598769",
      "source_type": "anime",
      "title": "Frieren: Beyond Journey's End",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "Frieren: Beyond Journey's End — 2023 Fall",
      "metadata": {
        "year": 2023,
        "season": "Fall",
        "title_jp": "葬送のフリーレン",
        "title_en": "Frieren: Beyond Journey's End",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Frieren: Beyond Journey's End\nJapanese title: 葬送のフリーレン\nYear: 2023\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Frieren: Beyond Journey's End — 2023 Fall — favourite."
      ]
    },
    {
      "id": "anime:2023-summer-fate-strange-fake-whisper-of-dawn-ba5d922988",
      "source_type": "anime",
      "title": "Fate/strange Fake -Whisper of Dawn-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Fate/strange Fake -Whisper of Dawn- — 2023 Summer",
      "metadata": {
        "year": 2023,
        "season": "Summer",
        "title_jp": "Fate/strange Fake -Whisper of Dawn-",
        "title_en": "Fate/strange Fake -Whisper of Dawn-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/strange Fake -Whisper of Dawn-\nJapanese title: Fate/strange Fake -Whisper of Dawn-\nYear: 2023\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/strange Fake -Whisper of Dawn- — 2023 Summer."
      ]
    },
    {
      "id": "anime:2023-summer-6d1e45fc0072-7407421134",
      "source_type": "anime",
      "title": "幻日夜羽 -鏡中暉光-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "幻日夜羽 -鏡中暉光- — 2023 Summer",
      "metadata": {
        "year": 2023,
        "season": "Summer",
        "title_jp": "幻日のヨハネ -SUNSHINE in the MIRROR-",
        "title_en": "幻日夜羽 -鏡中暉光-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 幻日夜羽 -鏡中暉光-\nJapanese title: 幻日のヨハネ -SUNSHINE in the MIRROR-\nYear: 2023\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "幻日夜羽 -鏡中暉光- — 2023 Summer."
      ]
    },
    {
      "id": "anime:2023-summer-bang-dream-it-s-mygo-157992f473",
      "source_type": "anime",
      "title": "BanG Dream! It's MyGO!!!!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "BanG Dream! It's MyGO!!!!! — 2023 Summer",
      "metadata": {
        "year": 2023,
        "season": "Summer",
        "title_jp": "BanG Dream! It's MyGO!!!!!",
        "title_en": "BanG Dream! It's MyGO!!!!!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: BanG Dream! It's MyGO!!!!!\nJapanese title: BanG Dream! It's MyGO!!!!!\nYear: 2023\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "BanG Dream! It's MyGO!!!!! — 2023 Summer."
      ]
    },
    {
      "id": "anime:2023-summer-masamune-kun-s-revenge-r-876144f898",
      "source_type": "anime",
      "title": "Masamune-kun's Revenge R",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Masamune-kun's Revenge R — 2023 Summer",
      "metadata": {
        "year": 2023,
        "season": "Summer",
        "title_jp": "政宗くんのリベンジR",
        "title_en": "Masamune-kun's Revenge R",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Masamune-kun's Revenge R\nJapanese title: 政宗くんのリベンジR\nYear: 2023\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Masamune-kun's Revenge R — 2023 Summer."
      ]
    },
    {
      "id": "anime:2023-summer-rent-a-girlfriend-s3-6015076490",
      "source_type": "anime",
      "title": "Rent-a-Girlfriend S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Rent-a-Girlfriend S3 — 2023 Summer",
      "metadata": {
        "year": 2023,
        "season": "Summer",
        "title_jp": "彼女、お借りします 3rd",
        "title_en": "Rent-a-Girlfriend S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rent-a-Girlfriend S3\nJapanese title: 彼女、お借りします 3rd\nYear: 2023\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rent-a-Girlfriend S3 — 2023 Summer."
      ]
    },
    {
      "id": "anime:2023-spring-ba4c5706b6fd-bc85b39d25",
      "source_type": "anime",
      "title": "為美好的世界獻上爆焰！",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "為美好的世界獻上爆焰！ — 2023 Spring",
      "metadata": {
        "year": 2023,
        "season": "Spring",
        "title_jp": "この素晴らしい世界に爆焔を！",
        "title_en": "為美好的世界獻上爆焰！",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 為美好的世界獻上爆焰！\nJapanese title: この素晴らしい世界に爆焔を！\nYear: 2023\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "為美好的世界獻上爆焰！ — 2023 Spring."
      ]
    },
    {
      "id": "anime:2023-spring-oshi-no-ko-cc7b786de0",
      "source_type": "anime",
      "title": "Oshi no Ko",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Oshi no Ko — 2023 Spring",
      "metadata": {
        "year": 2023,
        "season": "Spring",
        "title_jp": "【推しの子】",
        "title_en": "Oshi no Ko",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Oshi no Ko\nJapanese title: 【推しの子】\nYear: 2023\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Oshi no Ko — 2023 Spring."
      ]
    },
    {
      "id": "anime:2023-spring-the-final-season-4-3-e379cbc301",
      "source_type": "anime",
      "title": "進擊的巨人 The Final Season 完結篇（第4期第3部分）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2023",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "進擊的巨人 The Final Season 完結篇（第4期第3部分） — 2023 Spring",
      "metadata": {
        "year": 2023,
        "season": "Spring",
        "title_jp": "進撃の巨人 The Final Season 完結編",
        "title_en": "進擊的巨人 The Final Season 完結篇（第4期第3部分）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 進擊的巨人 The Final Season 完結篇（第4期第3部分）\nJapanese title: 進撃の巨人 The Final Season 完結編\nYear: 2023\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "進擊的巨人 The Final Season 完結篇（第4期第3部分） — 2023 Spring."
      ]
    },
    {
      "id": "anime:2022-film-suzume-8388e64610",
      "source_type": "anime",
      "title": "Suzume",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Suzume — 2022 Film",
      "metadata": {
        "year": 2022,
        "season": "Film",
        "title_jp": "すずめの戸締まり",
        "title_en": "Suzume",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Suzume\nJapanese title: すずめの戸締まり\nYear: 2022\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Suzume — 2022 Film."
      ]
    },
    {
      "id": "anime:2022-film-kaguya-sama-the-first-kiss-that-never-ends-f504689d70",
      "source_type": "anime",
      "title": "Kaguya-sama: The First Kiss That Never Ends",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Kaguya-sama: The First Kiss That Never Ends — 2022 Film",
      "metadata": {
        "year": 2022,
        "season": "Film",
        "title_jp": "かぐや様は告らせたい-ファーストキッスは終わらない-",
        "title_en": "Kaguya-sama: The First Kiss That Never Ends",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kaguya-sama: The First Kiss That Never Ends\nJapanese title: かぐや様は告らせたい-ファーストキッスは終わらない-\nYear: 2022\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kaguya-sama: The First Kiss That Never Ends — 2022 Film."
      ]
    },
    {
      "id": "anime:2022-fall-gundam-the-witch-from-mercury-12840e748e",
      "source_type": "anime",
      "title": "Gundam: The Witch from Mercury",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Gundam: The Witch from Mercury — 2022 Fall",
      "metadata": {
        "year": 2022,
        "season": "Fall",
        "title_jp": "機動戦士ガンダム 水星の魔女",
        "title_en": "Gundam: The Witch from Mercury",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Gundam: The Witch from Mercury\nJapanese title: 機動戦士ガンダム 水星の魔女\nYear: 2022\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam: The Witch from Mercury — 2022 Fall."
      ]
    },
    {
      "id": "anime:2022-fall-pop-team-epic-b8e8e93c31",
      "source_type": "anime",
      "title": "POP TEAM EPIC",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "POP TEAM EPIC — 2022 Fall",
      "metadata": {
        "year": 2022,
        "season": "Fall",
        "title_jp": "ポプテピピック TVアニメーション作品第二シリーズ",
        "title_en": "POP TEAM EPIC",
        "starred": false,
        "seichi": false
      },
      "content": "English title: POP TEAM EPIC\nJapanese title: ポプテピピック TVアニメーション作品第二シリーズ\nYear: 2022\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "POP TEAM EPIC — 2022 Fall."
      ]
    },
    {
      "id": "anime:2022-fall-next-summit-615dc56cce",
      "source_type": "anime",
      "title": "前進吧！登山少女 Next Summit",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "前進吧！登山少女 Next Summit — 2022 Fall",
      "metadata": {
        "year": 2022,
        "season": "Fall",
        "title_jp": "ヤマノススメ Next Summit",
        "title_en": "前進吧！登山少女 Next Summit",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 前進吧！登山少女 Next Summit\nJapanese title: ヤマノススメ Next Summit\nYear: 2022\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "前進吧！登山少女 Next Summit — 2022 Fall."
      ]
    },
    {
      "id": "anime:2022-summer-rent-a-girlfriend-s2-ba8c24d7d3",
      "source_type": "anime",
      "title": "Rent-a-Girlfriend S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Rent-a-Girlfriend S2 — 2022 Summer",
      "metadata": {
        "year": 2022,
        "season": "Summer",
        "title_jp": "彼女、お借りします 2nd",
        "title_en": "Rent-a-Girlfriend S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rent-a-Girlfriend S2\nJapanese title: 彼女、お借りします 2nd\nYear: 2022\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rent-a-Girlfriend S2 — 2022 Summer."
      ]
    },
    {
      "id": "anime:2022-summer-lycoris-recoil-f945be380e",
      "source_type": "anime",
      "title": "Lycoris Recoil",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Lycoris Recoil — 2022 Summer",
      "metadata": {
        "year": 2022,
        "season": "Summer",
        "title_jp": "リコリス・リコイル",
        "title_en": "Lycoris Recoil",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Lycoris Recoil\nJapanese title: リコリス・リコイル\nYear: 2022\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Lycoris Recoil — 2022 Summer."
      ]
    },
    {
      "id": "anime:2022-summer-love-live-superstar-552c77ce70",
      "source_type": "anime",
      "title": "Love Live! Superstar!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Love Live! Superstar!! — 2022 Summer",
      "metadata": {
        "year": 2022,
        "season": "Summer",
        "title_jp": "ラブライブ！スーパースター!!",
        "title_en": "Love Live! Superstar!!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Love Live! Superstar!!\nJapanese title: ラブライブ！スーパースター!!\nYear: 2022\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Love Live! Superstar!! — 2022 Summer."
      ]
    },
    {
      "id": "anime:2022-spring-summer-time-rendering-beed95f5a6",
      "source_type": "anime",
      "title": "Summer Time Rendering",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Summer Time Rendering — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "サマータイムレンダ",
        "title_en": "Summer Time Rendering",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Summer Time Rendering\nJapanese title: サマータイムレンダ\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Summer Time Rendering — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-spring-ya-boy-kongming-496139bf76",
      "source_type": "anime",
      "title": "Ya Boy Kongming!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Ya Boy Kongming! — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "パリピ孔明",
        "title_en": "Ya Boy Kongming!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Ya Boy Kongming!\nJapanese title: パリピ孔明\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Ya Boy Kongming! — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-spring-date-a-live-iv-e00684f30c",
      "source_type": "anime",
      "title": "Date A Live IV",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Date A Live IV — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "デート・ア・ライブIV",
        "title_en": "Date A Live IV",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Date A Live IV\nJapanese title: デート・ア・ライブIV\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Date A Live IV — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-spring-love-live-b1766c4bae",
      "source_type": "anime",
      "title": "Love Live! 虹咲學園學園偶像同好會",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Love Live! 虹咲學園學園偶像同好會 — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "ラブライブ！虹ヶ咲学園スクールアイドル同好会",
        "title_en": "Love Live! 虹咲學園學園偶像同好會",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Love Live! 虹咲學園學園偶像同好會\nJapanese title: ラブライブ！虹ヶ咲学園スクールアイドル同好会\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Love Live! 虹咲學園學園偶像同好會 — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-spring-final-season-52d1306566",
      "source_type": "anime",
      "title": "魔法紀錄 魔法少女小圓外傳 Final SEASON -淺夢的黎明-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "魔法紀錄 魔法少女小圓外傳 Final SEASON -淺夢的黎明- — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "マギアレコード 魔法少女まどか☆マギカ外伝 Final SEASON -浅き夢の暁-",
        "title_en": "魔法紀錄 魔法少女小圓外傳 Final SEASON -淺夢的黎明-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 魔法紀錄 魔法少女小圓外傳 Final SEASON -淺夢的黎明-\nJapanese title: マギアレコード 魔法少女まどか☆マギカ外伝 Final SEASON -浅き夢の暁-\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "魔法紀錄 魔法少女小圓外傳 Final SEASON -淺夢的黎明- — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-spring-f4c994533a16-bcee7aaaac",
      "source_type": "anime",
      "title": "輝夜姬想讓人告白-超級浪漫-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "輝夜姬想讓人告白-超級浪漫- — 2022 Spring",
      "metadata": {
        "year": 2022,
        "season": "Spring",
        "title_jp": "かぐや様は告らせたい-ウルトラロマンティック-",
        "title_en": "輝夜姬想讓人告白-超級浪漫-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 輝夜姬想讓人告白-超級浪漫-\nJapanese title: かぐや様は告らせたい-ウルトラロマンティック-\nYear: 2022\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "輝夜姬想讓人告白-超級浪漫- — 2022 Spring."
      ]
    },
    {
      "id": "anime:2022-winter-attack-on-titan-final-season-part-2-1ecc8474e3",
      "source_type": "anime",
      "title": "Attack on Titan: Final Season Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Attack on Titan: Final Season Part 2 — 2022 Winter",
      "metadata": {
        "year": 2022,
        "season": "Winter",
        "title_jp": "進撃の巨人 The Final Season Part 2",
        "title_en": "Attack on Titan: Final Season Part 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Attack on Titan: Final Season Part 2\nJapanese title: 進撃の巨人 The Final Season Part 2\nYear: 2022\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Attack on Titan: Final Season Part 2 — 2022 Winter."
      ]
    },
    {
      "id": "anime:2022-winter-my-dress-up-darling-0037f9fcc7",
      "source_type": "anime",
      "title": "My Dress-Up Darling",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2022",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "My Dress-Up Darling — 2022 Winter",
      "metadata": {
        "year": 2022,
        "season": "Winter",
        "title_jp": "その着せ替え人形は恋をする",
        "title_en": "My Dress-Up Darling",
        "starred": false,
        "seichi": false
      },
      "content": "English title: My Dress-Up Darling\nJapanese title: その着せ替え人形は恋をする\nYear: 2022\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "My Dress-Up Darling — 2022 Winter."
      ]
    },
    {
      "id": "anime:2021-film-fgo-camelot-part-2-0241ce9be2",
      "source_type": "anime",
      "title": "FGO: Camelot Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "FGO: Camelot Part 2 — 2021 Film",
      "metadata": {
        "year": 2021,
        "season": "Film",
        "title_jp": "劇場版 Fate/Grand Order -神聖円卓領域キャメロット- 後編",
        "title_en": "FGO: Camelot Part 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: FGO: Camelot Part 2\nJapanese title: 劇場版 Fate/Grand Order -神聖円卓領域キャメロット- 後編\nYear: 2021\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "FGO: Camelot Part 2 — 2021 Film."
      ]
    },
    {
      "id": "anime:2021-film-fgo-solomon-eb5a31330e",
      "source_type": "anime",
      "title": "FGO: Solomon",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "FGO: Solomon — 2021 Film",
      "metadata": {
        "year": 2021,
        "season": "Film",
        "title_jp": "劇場版 Fate/Grand Order -終局特異点 冠位時間神殿ソロモン-",
        "title_en": "FGO: Solomon",
        "starred": false,
        "seichi": false
      },
      "content": "English title: FGO: Solomon\nJapanese title: 劇場版 Fate/Grand Order -終局特異点 冠位時間神殿ソロモン-\nYear: 2021\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "FGO: Solomon — 2021 Film."
      ]
    },
    {
      "id": "anime:2021-fall-86-eighty-six-part-2-d0aa544919",
      "source_type": "anime",
      "title": "86 EIGHTY-SIX Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "86 EIGHTY-SIX Part 2 — 2021 Fall",
      "metadata": {
        "year": 2021,
        "season": "Fall",
        "title_jp": "86 Part 2",
        "title_en": "86 EIGHTY-SIX Part 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 86 EIGHTY-SIX Part 2\nJapanese title: 86 Part 2\nYear: 2021\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "86 EIGHTY-SIX Part 2 — 2021 Fall."
      ]
    },
    {
      "id": "anime:2021-summer-shiroi-suna-no-aquatope-1672f218a0",
      "source_type": "anime",
      "title": "Shiroi Suna no Aquatope",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Shiroi Suna no Aquatope — 2021 Summer",
      "metadata": {
        "year": 2021,
        "season": "Summer",
        "title_jp": "白い砂のアクアトープ",
        "title_en": "Shiroi Suna no Aquatope",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Shiroi Suna no Aquatope\nJapanese title: 白い砂のアクアトープ\nYear: 2021\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Shiroi Suna no Aquatope — 2021 Summer."
      ]
    },
    {
      "id": "anime:2021-summer-2nd-season-2-1a3ce8d6df",
      "source_type": "anime",
      "title": "魔法紀錄 魔法少女小圓外傳 2nd SEASON -覺醒前夜-（第2期）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "魔法紀錄 魔法少女小圓外傳 2nd SEASON -覺醒前夜-（第2期） — 2021 Summer",
      "metadata": {
        "year": 2021,
        "season": "Summer",
        "title_jp": "マギアレコード 魔法少女まどか☆マギカ外伝 2nd SEASON -覚醒前夜-",
        "title_en": "魔法紀錄 魔法少女小圓外傳 2nd SEASON -覺醒前夜-（第2期）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 魔法紀錄 魔法少女小圓外傳 2nd SEASON -覺醒前夜-（第2期）\nJapanese title: マギアレコード 魔法少女まどか☆マギカ外伝 2nd SEASON -覚醒前夜-\nYear: 2021\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "魔法紀錄 魔法少女小圓外傳 2nd SEASON -覺醒前夜-（第2期） — 2021 Summer."
      ]
    },
    {
      "id": "anime:2021-spring-86-eighty-six-adcf6e0ea8",
      "source_type": "anime",
      "title": "86 EIGHTY-SIX",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "86 EIGHTY-SIX — 2021 Spring",
      "metadata": {
        "year": 2021,
        "season": "Spring",
        "title_jp": "86―エイティシックス―",
        "title_en": "86 EIGHTY-SIX",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 86 EIGHTY-SIX\nJapanese title: 86―エイティシックス―\nYear: 2021\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "86 EIGHTY-SIX — 2021 Spring."
      ]
    },
    {
      "id": "anime:2021-winter-re-zero-s2-part-2-c1722c7ab7",
      "source_type": "anime",
      "title": "Re:ZERO S2 Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2021",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Re:ZERO S2 Part 2 — 2021 Winter",
      "metadata": {
        "year": 2021,
        "season": "Winter",
        "title_jp": "Re:ゼロから始める異世界生活 2 Part 2",
        "title_en": "Re:ZERO S2 Part 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:ZERO S2 Part 2\nJapanese title: Re:ゼロから始める異世界生活 2 Part 2\nYear: 2021\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:ZERO S2 Part 2 — 2021 Winter."
      ]
    },
    {
      "id": "anime:2020-ova-digimon-adventure-last-evolution-kizuna-0295cbee5f",
      "source_type": "anime",
      "title": "Digimon Adventure: Last Evolution Kizuna",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova",
        "favourite"
      ],
      "description": "Digimon Adventure: Last Evolution Kizuna — 2020 OVA",
      "metadata": {
        "year": 2020,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー LAST EVOLUTION 絆",
        "title_en": "Digimon Adventure: Last Evolution Kizuna",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Digimon Adventure: Last Evolution Kizuna\nJapanese title: デジモンアドベンチャー LAST EVOLUTION 絆\nYear: 2020\nSeason: OVA\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure: Last Evolution Kizuna — 2020 OVA — favourite."
      ]
    },
    {
      "id": "anime:2020-film-violet-evergarden-the-movie-39b1442660",
      "source_type": "anime",
      "title": "Violet Evergarden: The Movie",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite"
      ],
      "description": "Violet Evergarden: The Movie — 2020 Film",
      "metadata": {
        "year": 2020,
        "season": "Film",
        "title_jp": "ヴァイオレット・エヴァーガーデン 劇場版",
        "title_en": "Violet Evergarden: The Movie",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Violet Evergarden: The Movie\nJapanese title: ヴァイオレット・エヴァーガーデン 劇場版\nYear: 2020\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Violet Evergarden: The Movie — 2020 Film — favourite."
      ]
    },
    {
      "id": "anime:2020-film-hf-iii-spring-song-12443d6684",
      "source_type": "anime",
      "title": "HF III: spring song",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "HF III: spring song — 2020 Film",
      "metadata": {
        "year": 2020,
        "season": "Film",
        "title_jp": "Fate/stay night [Heaven's Feel] III. spring song",
        "title_en": "HF III: spring song",
        "starred": false,
        "seichi": false
      },
      "content": "English title: HF III: spring song\nJapanese title: Fate/stay night [Heaven's Feel] III. spring song\nYear: 2020\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "HF III: spring song — 2020 Film."
      ]
    },
    {
      "id": "anime:2020-film-fgo-camelot-part-1-8516054677",
      "source_type": "anime",
      "title": "FGO: Camelot Part 1",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "FGO: Camelot Part 1 — 2020 Film",
      "metadata": {
        "year": 2020,
        "season": "Film",
        "title_jp": "劇場版 Fate/Grand Order -神聖円卓領域キャメロット- 前編",
        "title_en": "FGO: Camelot Part 1",
        "starred": false,
        "seichi": false
      },
      "content": "English title: FGO: Camelot Part 1\nJapanese title: 劇場版 Fate/Grand Order -神聖円卓領域キャメロット- 前編\nYear: 2020\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "FGO: Camelot Part 1 — 2020 Film."
      ]
    },
    {
      "id": "anime:2020-film-shirobako-the-movie-3034cb6257",
      "source_type": "anime",
      "title": "SHIROBAKO: The Movie",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "SHIROBAKO: The Movie — 2020 Film",
      "metadata": {
        "year": 2020,
        "season": "Film",
        "title_jp": "劇場版 SHIROBAKO",
        "title_en": "SHIROBAKO: The Movie",
        "starred": false,
        "seichi": false
      },
      "content": "English title: SHIROBAKO: The Movie\nJapanese title: 劇場版 SHIROBAKO\nYear: 2020\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "SHIROBAKO: The Movie — 2020 Film."
      ]
    },
    {
      "id": "anime:2020-fall-attack-on-titan-the-final-season-f72c9704e8",
      "source_type": "anime",
      "title": "Attack on Titan: The Final Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Attack on Titan: The Final Season — 2020 Fall",
      "metadata": {
        "year": 2020,
        "season": "Fall",
        "title_jp": "進撃の巨人 The Final Season",
        "title_en": "Attack on Titan: The Final Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Attack on Titan: The Final Season\nJapanese title: 進撃の巨人 The Final Season\nYear: 2020\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Attack on Titan: The Final Season — 2020 Fall."
      ]
    },
    {
      "id": "anime:2020-fall-love-live-42b17f5bb8",
      "source_type": "anime",
      "title": "Love Live! 虹咲學園學園偶像同好會",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Love Live! 虹咲學園學園偶像同好會 — 2020 Fall",
      "metadata": {
        "year": 2020,
        "season": "Fall",
        "title_jp": "ラブライブ！虹ヶ咲学園スクールアイドル同好会",
        "title_en": "Love Live! 虹咲學園學園偶像同好會",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Love Live! 虹咲學園學園偶像同好會\nJapanese title: ラブライブ！虹ヶ咲学園スクールアイドル同好会\nYear: 2020\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Love Live! 虹咲學園學園偶像同好會 — 2020 Fall."
      ]
    },
    {
      "id": "anime:2020-fall-the-irregular-at-magic-high-school-visitor-arc-13c8844556",
      "source_type": "anime",
      "title": "The Irregular at Magic High School: Visitor Arc",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "The Irregular at Magic High School: Visitor Arc — 2020 Fall",
      "metadata": {
        "year": 2020,
        "season": "Fall",
        "title_jp": "魔法科高校の劣等生 来訪者編",
        "title_en": "The Irregular at Magic High School: Visitor Arc",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Irregular at Magic High School: Visitor Arc\nJapanese title: 魔法科高校の劣等生 来訪者編\nYear: 2020\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Irregular at Magic High School: Visitor Arc — 2020 Fall."
      ]
    },
    {
      "id": "anime:2020-summer-re-zero-s2-a304191dd4",
      "source_type": "anime",
      "title": "Re:ZERO S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Re:ZERO S2 — 2020 Summer",
      "metadata": {
        "year": 2020,
        "season": "Summer",
        "title_jp": "Re:ゼロから始める異世界生活 2",
        "title_en": "Re:ZERO S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:ZERO S2\nJapanese title: Re:ゼロから始める異世界生活 2\nYear: 2020\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:ZERO S2 — 2020 Summer."
      ]
    },
    {
      "id": "anime:2020-summer-rent-a-girlfriend-7563923b14",
      "source_type": "anime",
      "title": "Rent-a-Girlfriend",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Rent-a-Girlfriend — 2020 Summer",
      "metadata": {
        "year": 2020,
        "season": "Summer",
        "title_jp": "彼女、お借りします",
        "title_en": "Rent-a-Girlfriend",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rent-a-Girlfriend\nJapanese title: 彼女、お借りします\nYear: 2020\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rent-a-Girlfriend — 2020 Summer."
      ]
    },
    {
      "id": "anime:2020-summer-my-teen-romantic-comedy-snafu-climax-534c71c4c9",
      "source_type": "anime",
      "title": "My Teen Romantic Comedy SNAFU Climax",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite"
      ],
      "description": "My Teen Romantic Comedy SNAFU Climax — 2020 Summer",
      "metadata": {
        "year": 2020,
        "season": "Summer",
        "title_jp": "やはり俺の青春ラブコメはまちがっている。完",
        "title_en": "My Teen Romantic Comedy SNAFU Climax",
        "starred": true,
        "seichi": false
      },
      "content": "English title: My Teen Romantic Comedy SNAFU Climax\nJapanese title: やはり俺の青春ラブコメはまちがっている。完\nYear: 2020\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "My Teen Romantic Comedy SNAFU Climax — 2020 Summer — favourite."
      ]
    },
    {
      "id": "anime:2020-summer-sao-alicization-wou-part-2-de4088adb4",
      "source_type": "anime",
      "title": "SAO Alicization: WoU Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite"
      ],
      "description": "SAO Alicization: WoU Part 2 — 2020 Summer",
      "metadata": {
        "year": 2020,
        "season": "Summer",
        "title_jp": "ソードアート・オンライン アリシゼーション War of Underworld 2nd Season",
        "title_en": "SAO Alicization: WoU Part 2",
        "starred": true,
        "seichi": false
      },
      "content": "English title: SAO Alicization: WoU Part 2\nJapanese title: ソードアート・オンライン アリシゼーション War of Underworld 2nd Season\nYear: 2020\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "SAO Alicization: WoU Part 2 — 2020 Summer — favourite."
      ]
    },
    {
      "id": "anime:2020-spring-90416c8c4fbc-0f7adc8d5c",
      "source_type": "anime",
      "title": "數碼寶貝大冒險：",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "數碼寶貝大冒險： — 2020 Spring",
      "metadata": {
        "year": 2020,
        "season": "Spring",
        "title_jp": "デジモンアドベンチャー:",
        "title_en": "數碼寶貝大冒險：",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 數碼寶貝大冒險：\nJapanese title: デジモンアドベンチャー:\nYear: 2020\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "數碼寶貝大冒險： — 2020 Spring."
      ]
    },
    {
      "id": "anime:2020-spring-kaguya-sama-love-is-war-s2-daabd9deb8",
      "source_type": "anime",
      "title": "Kaguya-sama: Love is War S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Kaguya-sama: Love is War S2 — 2020 Spring",
      "metadata": {
        "year": 2020,
        "season": "Spring",
        "title_jp": "かぐや様は告らせたい？ S2",
        "title_en": "Kaguya-sama: Love is War S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kaguya-sama: Love is War S2\nJapanese title: かぐや様は告らせたい？ S2\nYear: 2020\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kaguya-sama: Love is War S2 — 2020 Spring."
      ]
    },
    {
      "id": "anime:2020-spring-kakushigoto-b01a68d338",
      "source_type": "anime",
      "title": "Kakushigoto",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Kakushigoto — 2020 Spring",
      "metadata": {
        "year": 2020,
        "season": "Spring",
        "title_jp": "かくしごと",
        "title_en": "Kakushigoto",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kakushigoto\nJapanese title: かくしごと\nYear: 2020\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kakushigoto — 2020 Spring."
      ]
    },
    {
      "id": "anime:2020-winter-bd37b4e838bd-349b03ebc0",
      "source_type": "anime",
      "title": "達爾文遊戲",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "達爾文遊戲 — 2020 Winter",
      "metadata": {
        "year": 2020,
        "season": "Winter",
        "title_jp": "ダーウィンズゲーム",
        "title_en": "達爾文遊戲",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 達爾文遊戲\nJapanese title: ダーウィンズゲーム\nYear: 2020\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "達爾文遊戲 — 2020 Winter."
      ]
    },
    {
      "id": "anime:2020-winter-47552cee3bd2-260b0e688e",
      "source_type": "anime",
      "title": "魔法紀錄 魔法少女小圓外傳",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "魔法紀錄 魔法少女小圓外傳 — 2020 Winter",
      "metadata": {
        "year": 2020,
        "season": "Winter",
        "title_jp": "マギアレコード 魔法少女まどか☆マギカ外伝",
        "title_en": "魔法紀錄 魔法少女小圓外傳",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 魔法紀錄 魔法少女小圓外傳\nJapanese title: マギアレコード 魔法少女まどか☆マギカ外伝\nYear: 2020\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "魔法紀錄 魔法少女小圓外傳 — 2020 Winter."
      ]
    },
    {
      "id": "anime:2020-winter-a-certain-scientific-railgun-t-3f317a882f",
      "source_type": "anime",
      "title": "A Certain Scientific Railgun T",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2020",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "A Certain Scientific Railgun T — 2020 Winter",
      "metadata": {
        "year": 2020,
        "season": "Winter",
        "title_jp": "とある科学の超電磁砲T",
        "title_en": "A Certain Scientific Railgun T",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Scientific Railgun T\nJapanese title: とある科学の超電磁砲T\nYear: 2020\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Scientific Railgun T — 2020 Winter."
      ]
    },
    {
      "id": "anime:2019-film-love-live-sunshine-over-the-rainbow-c26f8f3106",
      "source_type": "anime",
      "title": "Love Live! Sunshine!! Over the Rainbow",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "Love Live! Sunshine!! Over the Rainbow — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "劇場版 ラブライブ！サンシャイン!! Over the Rainbow",
        "title_en": "Love Live! Sunshine!! Over the Rainbow",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Love Live! Sunshine!! Over the Rainbow\nJapanese title: 劇場版 ラブライブ！サンシャイン!! Over the Rainbow\nYear: 2019\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Love Live! Sunshine!! Over the Rainbow — 2019 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2019-film-hf-ii-lost-butterfly-f3c303bfd5",
      "source_type": "anime",
      "title": "HF II: lost butterfly",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "HF II: lost butterfly — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "Fate/stay night [Heaven's Feel] II. lost butterfly",
        "title_en": "HF II: lost butterfly",
        "starred": false,
        "seichi": false
      },
      "content": "English title: HF II: lost butterfly\nJapanese title: Fate/stay night [Heaven's Feel] II. lost butterfly\nYear: 2019\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "HF II: lost butterfly — 2019 Film."
      ]
    },
    {
      "id": "anime:2019-film-rascal-does-not-dream-of-a-dreaming-girl-a5ba806196",
      "source_type": "anime",
      "title": "Rascal Does Not Dream of a Dreaming Girl",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Rascal Does Not Dream of a Dreaming Girl — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "劇場版 青春ブタ野郎はゆめみる少女の夢を見ない",
        "title_en": "Rascal Does Not Dream of a Dreaming Girl",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rascal Does Not Dream of a Dreaming Girl\nJapanese title: 劇場版 青春ブタ野郎はゆめみる少女の夢を見ない\nYear: 2019\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rascal Does Not Dream of a Dreaming Girl — 2019 Film."
      ]
    },
    {
      "id": "anime:2019-film-weathering-with-you-571672f2ec",
      "source_type": "anime",
      "title": "Weathering With You",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Weathering With You — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "天気の子",
        "title_en": "Weathering With You",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Weathering With You\nJapanese title: 天気の子\nYear: 2019\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Weathering With You — 2019 Film."
      ]
    },
    {
      "id": "anime:2019-film-saekano-fine-7238e9f9ad",
      "source_type": "anime",
      "title": "Saekano: Fine",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite"
      ],
      "description": "Saekano: Fine — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "劇場版 冴えない彼女の育てかた Fine",
        "title_en": "Saekano: Fine",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Saekano: Fine\nJapanese title: 劇場版 冴えない彼女の育てかた Fine\nYear: 2019\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Saekano: Fine — 2019 Film — favourite."
      ]
    },
    {
      "id": "anime:2019-film-hibike-euphonium-chikai-no-finale-9338952d92",
      "source_type": "anime",
      "title": "Hibike! Euphonium: Chikai no Finale",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "Hibike! Euphonium: Chikai no Finale — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "劇場版 響け！ユーフォニアム ~誓いのフィナーレ~",
        "title_en": "Hibike! Euphonium: Chikai no Finale",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Hibike! Euphonium: Chikai no Finale\nJapanese title: 劇場版 響け！ユーフォニアム ~誓いのフィナーレ~\nYear: 2019\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Hibike! Euphonium: Chikai no Finale — 2019 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2019-film-konosuba-legend-of-crimson-a2bf76088b",
      "source_type": "anime",
      "title": "KonoSuba: Legend of Crimson",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "KonoSuba: Legend of Crimson — 2019 Film",
      "metadata": {
        "year": 2019,
        "season": "Film",
        "title_jp": "劇場版 この素晴らしい世界に祝福を！紅伝説",
        "title_en": "KonoSuba: Legend of Crimson",
        "starred": false,
        "seichi": false
      },
      "content": "English title: KonoSuba: Legend of Crimson\nJapanese title: 劇場版 この素晴らしい世界に祝福を！紅伝説\nYear: 2019\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "KonoSuba: Legend of Crimson — 2019 Film."
      ]
    },
    {
      "id": "anime:2019-fall-c50c1a0456f8-cbc67abb20",
      "source_type": "anime",
      "title": "戰×戀",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "戰×戀 — 2019 Fall",
      "metadata": {
        "year": 2019,
        "season": "Fall",
        "title_jp": "戦×恋",
        "title_en": "戰×戀",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 戰×戀\nJapanese title: 戦×恋\nYear: 2019\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "戰×戀 — 2019 Fall."
      ]
    },
    {
      "id": "anime:2019-fall-fate-grand-order-c2fabd6a45",
      "source_type": "anime",
      "title": "Fate/Grand Order -絕對魔獸戰線巴比倫尼亞-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Fate/Grand Order -絕對魔獸戰線巴比倫尼亞- — 2019 Fall",
      "metadata": {
        "year": 2019,
        "season": "Fall",
        "title_jp": "Fate/Grand Order -絶対魔獣戦線バビロニア-",
        "title_en": "Fate/Grand Order -絕對魔獸戰線巴比倫尼亞-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/Grand Order -絕對魔獸戰線巴比倫尼亞-\nJapanese title: Fate/Grand Order -絶対魔獣戦線バビロニア-\nYear: 2019\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/Grand Order -絕對魔獸戰線巴比倫尼亞- — 2019 Fall."
      ]
    },
    {
      "id": "anime:2019-fall-alicization-war-of-underworld-6540249e62",
      "source_type": "anime",
      "title": "刀劍神域 Alicization War of Underworld",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "刀劍神域 Alicization War of Underworld — 2019 Fall",
      "metadata": {
        "year": 2019,
        "season": "Fall",
        "title_jp": "ソードアート・オンライン アリシゼーション War of Underworld",
        "title_en": "刀劍神域 Alicization War of Underworld",
        "starred": true,
        "seichi": false
      },
      "content": "English title: 刀劍神域 Alicization War of Underworld\nJapanese title: ソードアート・オンライン アリシゼーション War of Underworld\nYear: 2019\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域 Alicization War of Underworld — 2019 Fall — favourite."
      ]
    },
    {
      "id": "anime:2019-fall-psycho-pass-3-46ab4366ef",
      "source_type": "anime",
      "title": "PSYCHO-PASS 3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "PSYCHO-PASS 3 — 2019 Fall",
      "metadata": {
        "year": 2019,
        "season": "Fall",
        "title_jp": "PSYCHO-PASS サイコパス 3",
        "title_en": "PSYCHO-PASS 3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: PSYCHO-PASS 3\nJapanese title: PSYCHO-PASS サイコパス 3\nYear: 2019\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "PSYCHO-PASS 3 — 2019 Fall."
      ]
    },
    {
      "id": "anime:2019-summer-a-certain-scientific-accelerator-207cdbb18c",
      "source_type": "anime",
      "title": "A Certain Scientific Accelerator",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "A Certain Scientific Accelerator — 2019 Summer",
      "metadata": {
        "year": 2019,
        "season": "Summer",
        "title_jp": "とある科学の一方通行",
        "title_en": "A Certain Scientific Accelerator",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Scientific Accelerator\nJapanese title: とある科学の一方通行\nYear: 2019\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Scientific Accelerator — 2019 Summer."
      ]
    },
    {
      "id": "anime:2019-summer-ii-grace-note-9f6fe31132",
      "source_type": "anime",
      "title": "艾梅洛閣下II世事件簿 -魔眼搜集列車 Grace note-",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "艾梅洛閣下II世事件簿 -魔眼搜集列車 Grace note- — 2019 Summer",
      "metadata": {
        "year": 2019,
        "season": "Summer",
        "title_jp": "ロード・エルメロイⅡ世の事件簿 -魔眼蒐集列車 Grace note-",
        "title_en": "艾梅洛閣下II世事件簿 -魔眼搜集列車 Grace note-",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 艾梅洛閣下II世事件簿 -魔眼搜集列車 Grace note-\nJapanese title: ロード・エルメロイⅡ世の事件簿 -魔眼蒐集列車 Grace note-\nYear: 2019\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "艾梅洛閣下II世事件簿 -魔眼搜集列車 Grace note- — 2019 Summer."
      ]
    },
    {
      "id": "anime:2019-spring-season3-2c23e617a5",
      "source_type": "anime",
      "title": "進擊的巨人 Season3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "進擊的巨人 Season3 — 2019 Spring",
      "metadata": {
        "year": 2019,
        "season": "Spring",
        "title_jp": "進撃の巨人 Season3",
        "title_en": "進擊的巨人 Season3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 進擊的巨人 Season3\nJapanese title: 進撃の巨人 Season3\nYear: 2019\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "進擊的巨人 Season3 — 2019 Spring."
      ]
    },
    {
      "id": "anime:2019-winter-kaguya-sama-love-is-war-7a3045847e",
      "source_type": "anime",
      "title": "Kaguya-sama: Love is War",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Kaguya-sama: Love is War — 2019 Winter",
      "metadata": {
        "year": 2019,
        "season": "Winter",
        "title_jp": "かぐや様は告らせたい～天才たちの恋愛頭脳戦～",
        "title_en": "Kaguya-sama: Love is War",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kaguya-sama: Love is War\nJapanese title: かぐや様は告らせたい～天才たちの恋愛頭脳戦～\nYear: 2019\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kaguya-sama: Love is War — 2019 Winter."
      ]
    },
    {
      "id": "anime:2019-winter-the-quintessential-quintuplets-64ab4eafcf",
      "source_type": "anime",
      "title": "The Quintessential Quintuplets",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "The Quintessential Quintuplets — 2019 Winter",
      "metadata": {
        "year": 2019,
        "season": "Winter",
        "title_jp": "五等分の花嫁",
        "title_en": "The Quintessential Quintuplets",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Quintessential Quintuplets\nJapanese title: 五等分の花嫁\nYear: 2019\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Quintessential Quintuplets — 2019 Winter."
      ]
    },
    {
      "id": "anime:2019-winter-kakegurui-f35fadbd4d",
      "source_type": "anime",
      "title": "Kakegurui××",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Kakegurui×× — 2019 Winter",
      "metadata": {
        "year": 2019,
        "season": "Winter",
        "title_jp": "賭ケグルイ××",
        "title_en": "Kakegurui××",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kakegurui××\nJapanese title: 賭ケグルイ××\nYear: 2019\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kakegurui×× — 2019 Winter."
      ]
    },
    {
      "id": "anime:2019-winter-date-a-live-iii-8748106495",
      "source_type": "anime",
      "title": "Date A Live III",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Date A Live III — 2019 Winter",
      "metadata": {
        "year": 2019,
        "season": "Winter",
        "title_jp": "デート・ア・ライブIII",
        "title_en": "Date A Live III",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Date A Live III\nJapanese title: デート・ア・ライブIII\nYear: 2019\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Date A Live III — 2019 Winter."
      ]
    },
    {
      "id": "anime:2019-winter-bang-dream-2nd-season-410da61a32",
      "source_type": "anime",
      "title": "BanG Dream! 2nd Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2019",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "BanG Dream! 2nd Season — 2019 Winter",
      "metadata": {
        "year": 2019,
        "season": "Winter",
        "title_jp": "BanG Dream! 2nd Season",
        "title_en": "BanG Dream! 2nd Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: BanG Dream! 2nd Season\nJapanese title: BanG Dream! 2nd Season\nYear: 2019\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "BanG Dream! 2nd Season — 2019 Winter."
      ]
    },
    {
      "id": "anime:2018-ova-digimon-adventure-tri-6-future-0bf6758e98",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 6: Future",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 6: Future — 2018 OVA",
      "metadata": {
        "year": 2018,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第6章「未来」",
        "title_en": "Digimon Adventure tri. 6: Future",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 6: Future\nJapanese title: デジモンアドベンチャー tri. 第6章「未来」\nYear: 2018\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 6: Future — 2018 OVA."
      ]
    },
    {
      "id": "anime:2018-film-gundam-nt-narrative-c8140cbd86",
      "source_type": "anime",
      "title": "Gundam NT (Narrative)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Gundam NT (Narrative) — 2018 Film",
      "metadata": {
        "year": 2018,
        "season": "Film",
        "title_jp": "機動戦士ガンダムNT",
        "title_en": "Gundam NT (Narrative)",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Gundam NT (Narrative)\nJapanese title: 機動戦士ガンダムNT\nYear: 2018\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam NT (Narrative) — 2018 Film."
      ]
    },
    {
      "id": "anime:2018-fall-rascal-does-not-dream-of-bunny-girl-senpai-00d41a3d5c",
      "source_type": "anime",
      "title": "Rascal Does Not Dream of Bunny Girl Senpai",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Rascal Does Not Dream of Bunny Girl Senpai — 2018 Fall",
      "metadata": {
        "year": 2018,
        "season": "Fall",
        "title_jp": "青春ブタ野郎はバニーガール先輩の夢を見ない",
        "title_en": "Rascal Does Not Dream of Bunny Girl Senpai",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rascal Does Not Dream of Bunny Girl Senpai\nJapanese title: 青春ブタ野郎はバニーガール先輩の夢を見ない\nYear: 2018\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rascal Does Not Dream of Bunny Girl Senpai — 2018 Fall."
      ]
    },
    {
      "id": "anime:2018-fall-iroduku-the-world-in-colors-3c366bed82",
      "source_type": "anime",
      "title": "Iroduku: The World in Colors",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "Iroduku: The World in Colors — 2018 Fall",
      "metadata": {
        "year": 2018,
        "season": "Fall",
        "title_jp": "色づく世界の明日から",
        "title_en": "Iroduku: The World in Colors",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Iroduku: The World in Colors\nJapanese title: 色づく世界の明日から\nYear: 2018\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Iroduku: The World in Colors — 2018 Fall — favourite."
      ]
    },
    {
      "id": "anime:2018-fall-a-certain-magical-index-iii-d9bbdfcc9a",
      "source_type": "anime",
      "title": "A Certain Magical Index III",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "A Certain Magical Index III — 2018 Fall",
      "metadata": {
        "year": 2018,
        "season": "Fall",
        "title_jp": "とある魔術の禁書目録III",
        "title_en": "A Certain Magical Index III",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Magical Index III\nJapanese title: とある魔術の禁書目録III\nYear: 2018\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Magical Index III — 2018 Fall."
      ]
    },
    {
      "id": "anime:2018-fall-alicization-ca0938c094",
      "source_type": "anime",
      "title": "刀劍神域 Alicization",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "刀劍神域 Alicization — 2018 Fall",
      "metadata": {
        "year": 2018,
        "season": "Fall",
        "title_jp": "ソードアート・オンライン アリシゼーション",
        "title_en": "刀劍神域 Alicization",
        "starred": true,
        "seichi": false
      },
      "content": "English title: 刀劍神域 Alicization\nJapanese title: ソードアート・オンライン アリシゼーション\nYear: 2018\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域 Alicization — 2018 Fall — favourite."
      ]
    },
    {
      "id": "anime:2018-summer-attack-on-titan-s3-98a12ec9ae",
      "source_type": "anime",
      "title": "Attack on Titan S3",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Attack on Titan S3 — 2018 Summer",
      "metadata": {
        "year": 2018,
        "season": "Summer",
        "title_jp": "進撃の巨人 Season 3",
        "title_en": "Attack on Titan S3",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Attack on Titan S3\nJapanese title: 進撃の巨人 Season 3\nYear: 2018\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Attack on Titan S3 — 2018 Summer."
      ]
    },
    {
      "id": "anime:2018-summer-revue-starlight-fd5acd38db",
      "source_type": "anime",
      "title": "Revue Starlight",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Revue Starlight — 2018 Summer",
      "metadata": {
        "year": 2018,
        "season": "Summer",
        "title_jp": "少女☆歌劇 レヴュー・スタァライト",
        "title_en": "Revue Starlight",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Revue Starlight\nJapanese title: 少女☆歌劇 レヴュー・スタァライト\nYear: 2018\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Revue Starlight — 2018 Summer."
      ]
    },
    {
      "id": "anime:2018-summer-third-season-9b754b8814",
      "source_type": "anime",
      "title": "前進吧！登山少女 Third Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "前進吧！登山少女 Third Season — 2018 Summer",
      "metadata": {
        "year": 2018,
        "season": "Summer",
        "title_jp": "ヤマノススメ サードシーズン",
        "title_en": "前進吧！登山少女 Third Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 前進吧！登山少女 Third Season\nJapanese title: ヤマノススメ サードシーズン\nYear: 2018\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "前進吧！登山少女 Third Season — 2018 Summer."
      ]
    },
    {
      "id": "anime:2018-spring-gun-gale-online-8e23e518be",
      "source_type": "anime",
      "title": "刀劍神域外傳Gun Gale Online",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "刀劍神域外傳Gun Gale Online — 2018 Spring",
      "metadata": {
        "year": 2018,
        "season": "Spring",
        "title_jp": "ソードアート・オンライン オルタナティブ ガンゲイル・オンライン",
        "title_en": "刀劍神域外傳Gun Gale Online",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 刀劍神域外傳Gun Gale Online\nJapanese title: ソードアート・オンライン オルタナティブ ガンゲイル・オンライン\nYear: 2018\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域外傳Gun Gale Online — 2018 Spring."
      ]
    },
    {
      "id": "anime:2018-winter-violet-evergarden-0fe0040629",
      "source_type": "anime",
      "title": "Violet Evergarden",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "favourite"
      ],
      "description": "Violet Evergarden — 2018 Winter",
      "metadata": {
        "year": 2018,
        "season": "Winter",
        "title_jp": "ヴァイオレット・エヴァーガーデン",
        "title_en": "Violet Evergarden",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Violet Evergarden\nJapanese title: ヴァイオレット・エヴァーガーデン\nYear: 2018\nSeason: Winter\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Violet Evergarden — 2018 Winter — favourite."
      ]
    },
    {
      "id": "anime:2018-winter-fate-extra-last-encore-de361bb36c",
      "source_type": "anime",
      "title": "Fate/EXTRA Last Encore",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Fate/EXTRA Last Encore — 2018 Winter",
      "metadata": {
        "year": 2018,
        "season": "Winter",
        "title_jp": "Fate/EXTRA Last Encore",
        "title_en": "Fate/EXTRA Last Encore",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/EXTRA Last Encore\nJapanese title: Fate/EXTRA Last Encore\nYear: 2018\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/EXTRA Last Encore — 2018 Winter."
      ]
    },
    {
      "id": "anime:2018-winter-pop-team-epic-53a8e3b222",
      "source_type": "anime",
      "title": "Pop Team Epic",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Pop Team Epic — 2018 Winter",
      "metadata": {
        "year": 2018,
        "season": "Winter",
        "title_jp": "ポプテピピック",
        "title_en": "Pop Team Epic",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Pop Team Epic\nJapanese title: ポプテピピック\nYear: 2018\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Pop Team Epic — 2018 Winter."
      ]
    },
    {
      "id": "anime:2018-winter-darling-in-the-franxx-93d9d8d5f6",
      "source_type": "anime",
      "title": "DARLING in the FRANXX",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2018",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "DARLING in the FRANXX — 2018 Winter",
      "metadata": {
        "year": 2018,
        "season": "Winter",
        "title_jp": "ダーリン・イン・ザ・フランキス",
        "title_en": "DARLING in the FRANXX",
        "starred": false,
        "seichi": false
      },
      "content": "English title: DARLING in the FRANXX\nJapanese title: ダーリン・イン・ザ・フランキス\nYear: 2018\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "DARLING in the FRANXX — 2018 Winter."
      ]
    },
    {
      "id": "anime:2017-ova-digimon-adventure-tri-4-loss-51066d034a",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 4: Loss",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 4: Loss — 2017 OVA",
      "metadata": {
        "year": 2017,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第4章「喪失」",
        "title_en": "Digimon Adventure tri. 4: Loss",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 4: Loss\nJapanese title: デジモンアドベンチャー tri. 第4章「喪失」\nYear: 2017\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 4: Loss — 2017 OVA."
      ]
    },
    {
      "id": "anime:2017-ova-digimon-adventure-tri-5-coexistence-14a496dc0e",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 5: Coexistence",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 5: Coexistence — 2017 OVA",
      "metadata": {
        "year": 2017,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第5章「共生」",
        "title_en": "Digimon Adventure tri. 5: Coexistence",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 5: Coexistence\nJapanese title: デジモンアドベンチャー tri. 第5章「共生」\nYear: 2017\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 5: Coexistence — 2017 OVA."
      ]
    },
    {
      "id": "anime:2017-film-hf-i-presage-flower-4f8d2dd852",
      "source_type": "anime",
      "title": "HF I: presage flower",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "HF I: presage flower — 2017 Film",
      "metadata": {
        "year": 2017,
        "season": "Film",
        "title_jp": "Fate/stay night [Heaven's Feel] I. presage flower",
        "title_en": "HF I: presage flower",
        "starred": false,
        "seichi": false
      },
      "content": "English title: HF I: presage flower\nJapanese title: Fate/stay night [Heaven's Feel] I. presage flower\nYear: 2017\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "HF I: presage flower — 2017 Film."
      ]
    },
    {
      "id": "anime:2017-film-hibike-euphonium-todoketai-melody-28d880620f",
      "source_type": "anime",
      "title": "Hibike! Euphonium: Todoketai Melody",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "Hibike! Euphonium: Todoketai Melody — 2017 Film",
      "metadata": {
        "year": 2017,
        "season": "Film",
        "title_jp": "劇場版 響け！ユーフォニアム ~届けたいメロディ~",
        "title_en": "Hibike! Euphonium: Todoketai Melody",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Hibike! Euphonium: Todoketai Melody\nJapanese title: 劇場版 響け！ユーフォニアム ~届けたいメロディ~\nYear: 2017\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Hibike! Euphonium: Todoketai Melody — 2017 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2017-fall-lovelive-sunshine-9ba30427cf",
      "source_type": "anime",
      "title": "LoveLive! Sunshine!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "LoveLive! Sunshine!! — 2017 Fall",
      "metadata": {
        "year": 2017,
        "season": "Fall",
        "title_jp": "ラブライブ！サンシャイン!!",
        "title_en": "LoveLive! Sunshine!!",
        "starred": true,
        "seichi": true
      },
      "content": "English title: LoveLive! Sunshine!!\nJapanese title: ラブライブ！サンシャイン!!\nYear: 2017\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "LoveLive! Sunshine!! — 2017 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2017-summer-kakegurui-1d9517e753",
      "source_type": "anime",
      "title": "Kakegurui",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Kakegurui — 2017 Summer",
      "metadata": {
        "year": 2017,
        "season": "Summer",
        "title_jp": "賭ケグルイ",
        "title_en": "Kakegurui",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kakegurui\nJapanese title: 賭ケグルイ\nYear: 2017\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kakegurui — 2017 Summer."
      ]
    },
    {
      "id": "anime:2017-summer-fate-apocrypha-59051e51e0",
      "source_type": "anime",
      "title": "Fate/Apocrypha",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Fate/Apocrypha — 2017 Summer",
      "metadata": {
        "year": 2017,
        "season": "Summer",
        "title_jp": "Fate/Apocrypha",
        "title_en": "Fate/Apocrypha",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/Apocrypha\nJapanese title: Fate/Apocrypha\nYear: 2017\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/Apocrypha — 2017 Summer."
      ]
    },
    {
      "id": "anime:2017-spring-eromanga-sensei-632d4d6d43",
      "source_type": "anime",
      "title": "Eromanga Sensei",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Eromanga Sensei — 2017 Spring",
      "metadata": {
        "year": 2017,
        "season": "Spring",
        "title_jp": "エロマンガ先生",
        "title_en": "Eromanga Sensei",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Eromanga Sensei\nJapanese title: エロマンガ先生\nYear: 2017\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Eromanga Sensei — 2017 Spring."
      ]
    },
    {
      "id": "anime:2017-spring-sagrada-reset-d950582be2",
      "source_type": "anime",
      "title": "Sagrada Reset",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "visited"
      ],
      "description": "Sagrada Reset — 2017 Spring",
      "metadata": {
        "year": 2017,
        "season": "Spring",
        "title_jp": "サクラダリセット",
        "title_en": "Sagrada Reset",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Sagrada Reset\nJapanese title: サクラダリセット\nYear: 2017\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Sagrada Reset — 2017 Spring — location visited."
      ]
    },
    {
      "id": "anime:2017-spring-saekano-how-to-raise-a-boring-girlfriend-1821bb4815",
      "source_type": "anime",
      "title": "Saekano: How to Raise a Boring Girlfriend ♭",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "Saekano: How to Raise a Boring Girlfriend ♭ — 2017 Spring",
      "metadata": {
        "year": 2017,
        "season": "Spring",
        "title_jp": "冴えない彼女の育てかた♭",
        "title_en": "Saekano: How to Raise a Boring Girlfriend ♭",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Saekano: How to Raise a Boring Girlfriend ♭\nJapanese title: 冴えない彼女の育てかた♭\nYear: 2017\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Saekano: How to Raise a Boring Girlfriend ♭ — 2017 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2017-spring-season2-2fbf0c641e",
      "source_type": "anime",
      "title": "進擊的巨人 Season2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "進擊的巨人 Season2 — 2017 Spring",
      "metadata": {
        "year": 2017,
        "season": "Spring",
        "title_jp": "進撃の巨人 Season2",
        "title_en": "進擊的巨人 Season2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 進擊的巨人 Season2\nJapanese title: 進撃の巨人 Season2\nYear: 2017\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "進擊的巨人 Season2 — 2017 Spring."
      ]
    },
    {
      "id": "anime:2017-spring-7c691df10ce7-b036831b4d",
      "source_type": "anime",
      "title": "時鐘機關之星",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "時鐘機關之星 — 2017 Spring",
      "metadata": {
        "year": 2017,
        "season": "Spring",
        "title_jp": "クロックワーク・プラネット",
        "title_en": "時鐘機關之星",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 時鐘機關之星\nJapanese title: クロックワーク・プラネット\nYear: 2017\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "時鐘機關之星 — 2017 Spring."
      ]
    },
    {
      "id": "anime:2017-winter-konosuba-s2-759ce3ff23",
      "source_type": "anime",
      "title": "KonoSuba S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "KonoSuba S2 — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "この素晴らしい世界に祝福を！2",
        "title_en": "KonoSuba S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: KonoSuba S2\nJapanese title: この素晴らしい世界に祝福を！2\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "KonoSuba S2 — 2017 Winter."
      ]
    },
    {
      "id": "anime:2017-winter-masamune-kun-s-revenge-6b660c0ec8",
      "source_type": "anime",
      "title": "Masamune-kun's Revenge",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Masamune-kun's Revenge — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "政宗くんのリベンジ",
        "title_en": "Masamune-kun's Revenge",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Masamune-kun's Revenge\nJapanese title: 政宗くんのリベンジ\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Masamune-kun's Revenge — 2017 Winter."
      ]
    },
    {
      "id": "anime:2017-winter-scum-s-wish-e66f9a3538",
      "source_type": "anime",
      "title": "Scum's Wish",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Scum's Wish — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "クズの本懐",
        "title_en": "Scum's Wish",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Scum's Wish\nJapanese title: クズの本懐\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Scum's Wish — 2017 Winter."
      ]
    },
    {
      "id": "anime:2017-winter-kemono-friends-1279e0542d",
      "source_type": "anime",
      "title": "Kemono Friends",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Kemono Friends — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "けものフレンズ",
        "title_en": "Kemono Friends",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kemono Friends\nJapanese title: けものフレンズ\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kemono Friends — 2017 Winter."
      ]
    },
    {
      "id": "anime:2017-winter-bang-dream-79b5dc5c96",
      "source_type": "anime",
      "title": "BanG Dream!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "BanG Dream! — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "BanG Dream!",
        "title_en": "BanG Dream!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: BanG Dream!\nJapanese title: BanG Dream!\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "BanG Dream! — 2017 Winter."
      ]
    },
    {
      "id": "anime:2017-winter-rewrite-a44245dabb",
      "source_type": "anime",
      "title": "Rewrite",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2017",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Rewrite — 2017 Winter",
      "metadata": {
        "year": 2017,
        "season": "Winter",
        "title_jp": "Rewrite",
        "title_en": "Rewrite",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rewrite\nJapanese title: Rewrite\nYear: 2017\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rewrite — 2017 Winter."
      ]
    },
    {
      "id": "anime:2016-ova-digimon-adventure-tri-2-determination-c5e4c82814",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 2: Determination",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 2: Determination — 2016 OVA",
      "metadata": {
        "year": 2016,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第2章「決意」",
        "title_en": "Digimon Adventure tri. 2: Determination",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 2: Determination\nJapanese title: デジモンアドベンチャー tri. 第2章「決意」\nYear: 2016\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 2: Determination — 2016 OVA."
      ]
    },
    {
      "id": "anime:2016-ova-digimon-adventure-tri-3-confession-34e1a56c3e",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 3: Confession",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 3: Confession — 2016 OVA",
      "metadata": {
        "year": 2016,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第3章「告白」",
        "title_en": "Digimon Adventure tri. 3: Confession",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 3: Confession\nJapanese title: デジモンアドベンチャー tri. 第3章「告白」\nYear: 2016\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 3: Confession — 2016 OVA."
      ]
    },
    {
      "id": "anime:2016-film-your-name-31fcd29c8a",
      "source_type": "anime",
      "title": "Your Name",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Your Name — 2016 Film",
      "metadata": {
        "year": 2016,
        "season": "Film",
        "title_jp": "君の名は。",
        "title_en": "Your Name",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Your Name\nJapanese title: 君の名は。\nYear: 2016\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Your Name — 2016 Film."
      ]
    },
    {
      "id": "anime:2016-fall-sound-euphonium-2-d2f2fd8dcb",
      "source_type": "anime",
      "title": "Sound! Euphonium 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Sound! Euphonium 2 — 2016 Fall",
      "metadata": {
        "year": 2016,
        "season": "Fall",
        "title_jp": "響け！ユーフォニアム2",
        "title_en": "Sound! Euphonium 2",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Sound! Euphonium 2\nJapanese title: 響け！ユーフォニアム2\nYear: 2016\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Sound! Euphonium 2 — 2016 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2016-fall-gundam-iron-blooded-orphans-s2-c063e4207b",
      "source_type": "anime",
      "title": "Gundam: Iron-Blooded Orphans S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Gundam: Iron-Blooded Orphans S2 — 2016 Fall",
      "metadata": {
        "year": 2016,
        "season": "Fall",
        "title_jp": "機動戦士ガンダム 鉄血のオルフェンズ 2nd",
        "title_en": "Gundam: Iron-Blooded Orphans S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Gundam: Iron-Blooded Orphans S2\nJapanese title: 機動戦士ガンダム 鉄血のオルフェンズ 2nd\nYear: 2016\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam: Iron-Blooded Orphans S2 — 2016 Fall."
      ]
    },
    {
      "id": "anime:2016-summer-fate-kaleid-liner-3rei-11c9cabe22",
      "source_type": "anime",
      "title": "Fate/kaleid liner 魔法少女☆伊莉雅 3rei!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Fate/kaleid liner 魔法少女☆伊莉雅 3rei!! — 2016 Summer",
      "metadata": {
        "year": 2016,
        "season": "Summer",
        "title_jp": "Fate/kaleid liner プリズマ☆イリヤ ドライ!!",
        "title_en": "Fate/kaleid liner 魔法少女☆伊莉雅 3rei!!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/kaleid liner 魔法少女☆伊莉雅 3rei!!\nJapanese title: Fate/kaleid liner プリズマ☆イリヤ ドライ!!\nYear: 2016\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/kaleid liner 魔法少女☆伊莉雅 3rei!! — 2016 Summer."
      ]
    },
    {
      "id": "anime:2016-summer-love-live-sunshine-fb66f7e919",
      "source_type": "anime",
      "title": "Love Live! Sunshine!!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite",
        "visited"
      ],
      "description": "Love Live! Sunshine!! — 2016 Summer",
      "metadata": {
        "year": 2016,
        "season": "Summer",
        "title_jp": "ラブライブ！サンシャイン!!",
        "title_en": "Love Live! Sunshine!!",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Love Live! Sunshine!!\nJapanese title: ラブライブ！サンシャイン!!\nYear: 2016\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Love Live! Sunshine!! — 2016 Summer — favourite — location visited."
      ]
    },
    {
      "id": "anime:2016-summer-rewrite-4a747a418b",
      "source_type": "anime",
      "title": "Rewrite",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Rewrite — 2016 Summer",
      "metadata": {
        "year": 2016,
        "season": "Summer",
        "title_jp": "リライト",
        "title_en": "Rewrite",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rewrite\nJapanese title: リライト\nYear: 2016\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rewrite — 2016 Summer."
      ]
    },
    {
      "id": "anime:2016-spring-re-zero-starting-life-in-another-world-029b1ffdcb",
      "source_type": "anime",
      "title": "Re:ZERO − Starting Life in Another World",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Re:ZERO − Starting Life in Another World — 2016 Spring",
      "metadata": {
        "year": 2016,
        "season": "Spring",
        "title_jp": "Re:ゼロから始める異世界生活",
        "title_en": "Re:ZERO − Starting Life in Another World",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Re:ZERO − Starting Life in Another World\nJapanese title: Re:ゼロから始める異世界生活\nYear: 2016\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Re:ZERO − Starting Life in Another World — 2016 Spring."
      ]
    },
    {
      "id": "anime:2016-spring-mayoiga-f2f61fb2e8",
      "source_type": "anime",
      "title": "Mayoiga",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Mayoiga — 2016 Spring",
      "metadata": {
        "year": 2016,
        "season": "Spring",
        "title_jp": "迷家-マヨイガ-",
        "title_en": "Mayoiga",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Mayoiga\nJapanese title: 迷家-マヨイガ-\nYear: 2016\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Mayoiga — 2016 Spring."
      ]
    },
    {
      "id": "anime:2016-winter-konosuba-god-s-blessing-on-this-wonderful-world-ca924d24fc",
      "source_type": "anime",
      "title": "KonoSuba: God's Blessing on This Wonderful World!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "KonoSuba: God's Blessing on This Wonderful World! — 2016 Winter",
      "metadata": {
        "year": 2016,
        "season": "Winter",
        "title_jp": "この素晴らしい世界に祝福を！",
        "title_en": "KonoSuba: God's Blessing on This Wonderful World!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: KonoSuba: God's Blessing on This Wonderful World!\nJapanese title: この素晴らしい世界に祝福を！\nYear: 2016\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "KonoSuba: God's Blessing on This Wonderful World! — 2016 Winter."
      ]
    },
    {
      "id": "anime:2016-winter-musaigen-no-phantom-world-97cc3b5434",
      "source_type": "anime",
      "title": "Musaigen no Phantom World",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2016",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Musaigen no Phantom World — 2016 Winter",
      "metadata": {
        "year": 2016,
        "season": "Winter",
        "title_jp": "無彩限のファントム・ワールド",
        "title_en": "Musaigen no Phantom World",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Musaigen no Phantom World\nJapanese title: 無彩限のファントム・ワールド\nYear: 2016\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Musaigen no Phantom World — 2016 Winter."
      ]
    },
    {
      "id": "anime:2015-ova-digimon-adventure-tri-1-reunion-8fd4c114b8",
      "source_type": "anime",
      "title": "Digimon Adventure tri. 1: Reunion",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "Digimon Adventure tri. 1: Reunion — 2015 OVA",
      "metadata": {
        "year": 2015,
        "season": "OVA",
        "title_jp": "デジモンアドベンチャー tri. 第1章「再会」",
        "title_en": "Digimon Adventure tri. 1: Reunion",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Digimon Adventure tri. 1: Reunion\nJapanese title: デジモンアドベンチャー tri. 第1章「再会」\nYear: 2015\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Digimon Adventure tri. 1: Reunion — 2015 OVA."
      ]
    },
    {
      "id": "anime:2015-film-love-live-the-school-idol-movie-5f771596fe",
      "source_type": "anime",
      "title": "Love Live! The School Idol Movie",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "Love Live! The School Idol Movie — 2015 Film",
      "metadata": {
        "year": 2015,
        "season": "Film",
        "title_jp": "劇場版 ラブライブ! The School Idol Movie",
        "title_en": "Love Live! The School Idol Movie",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Love Live! The School Idol Movie\nJapanese title: 劇場版 ラブライブ! The School Idol Movie\nYear: 2015\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Love Live! The School Idol Movie — 2015 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2015-film-psycho-pass-the-movie-ea26e07f57",
      "source_type": "anime",
      "title": "PSYCHO-PASS: The Movie",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "PSYCHO-PASS: The Movie — 2015 Film",
      "metadata": {
        "year": 2015,
        "season": "Film",
        "title_jp": "劇場版 PSYCHO-PASS サイコパス",
        "title_en": "PSYCHO-PASS: The Movie",
        "starred": false,
        "seichi": false
      },
      "content": "English title: PSYCHO-PASS: The Movie\nJapanese title: 劇場版 PSYCHO-PASS サイコパス\nYear: 2015\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "PSYCHO-PASS: The Movie — 2015 Film."
      ]
    },
    {
      "id": "anime:2015-fall-owarimonogatari-00b2b42ab2",
      "source_type": "anime",
      "title": "Owarimonogatari",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Owarimonogatari — 2015 Fall",
      "metadata": {
        "year": 2015,
        "season": "Fall",
        "title_jp": "終物語",
        "title_en": "Owarimonogatari",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Owarimonogatari\nJapanese title: 終物語\nYear: 2015\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Owarimonogatari — 2015 Fall."
      ]
    },
    {
      "id": "anime:2015-fall-gundam-iron-blooded-orphans-6891de4237",
      "source_type": "anime",
      "title": "Gundam: Iron-Blooded Orphans",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Gundam: Iron-Blooded Orphans — 2015 Fall",
      "metadata": {
        "year": 2015,
        "season": "Fall",
        "title_jp": "機動戦士ガンダム 鉄血のオルフェンズ",
        "title_en": "Gundam: Iron-Blooded Orphans",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Gundam: Iron-Blooded Orphans\nJapanese title: 機動戦士ガンダム 鉄血のオルフェンズ\nYear: 2015\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam: Iron-Blooded Orphans — 2015 Fall."
      ]
    },
    {
      "id": "anime:2015-fall-is-the-order-a-rabbit-s2-6d6e2e1730",
      "source_type": "anime",
      "title": "Is the Order a Rabbit?? S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Is the Order a Rabbit?? S2 — 2015 Fall",
      "metadata": {
        "year": 2015,
        "season": "Fall",
        "title_jp": "ご注文はうさぎですか??",
        "title_en": "Is the Order a Rabbit?? S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Is the Order a Rabbit?? S2\nJapanese title: ご注文はうさぎですか??\nYear: 2015\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Is the Order a Rabbit?? S2 — 2015 Fall."
      ]
    },
    {
      "id": "anime:2015-summer-charlotte-5476c6d2c5",
      "source_type": "anime",
      "title": "Charlotte",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Charlotte — 2015 Summer",
      "metadata": {
        "year": 2015,
        "season": "Summer",
        "title_jp": "シャーロット",
        "title_en": "Charlotte",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Charlotte\nJapanese title: シャーロット\nYear: 2015\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Charlotte — 2015 Summer."
      ]
    },
    {
      "id": "anime:2015-summer-darkness-2-4e1691b4f6",
      "source_type": "anime",
      "title": "出包王女DARKNESS（第2期）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "出包王女DARKNESS（第2期） — 2015 Summer",
      "metadata": {
        "year": 2015,
        "season": "Summer",
        "title_jp": "To LOVEる -とらぶる- ダークネス-2nd",
        "title_en": "出包王女DARKNESS（第2期）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 出包王女DARKNESS（第2期）\nJapanese title: To LOVEる -とらぶる- ダークネス-2nd\nYear: 2015\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "出包王女DARKNESS（第2期） — 2015 Summer."
      ]
    },
    {
      "id": "anime:2015-spring-my-teen-romantic-comedy-snafu-too-3051b92908",
      "source_type": "anime",
      "title": "My Teen Romantic Comedy SNAFU Too",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "My Teen Romantic Comedy SNAFU Too — 2015 Spring",
      "metadata": {
        "year": 2015,
        "season": "Spring",
        "title_jp": "やはり俺の青春ラブコメはまちがっている。続",
        "title_en": "My Teen Romantic Comedy SNAFU Too",
        "starred": true,
        "seichi": true
      },
      "content": "English title: My Teen Romantic Comedy SNAFU Too\nJapanese title: やはり俺の青春ラブコメはまちがっている。続\nYear: 2015\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "My Teen Romantic Comedy SNAFU Too — 2015 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2015-spring-fate-stay-night-ubw-2nd-season-6ca36023d5",
      "source_type": "anime",
      "title": "Fate/stay night UBW 2nd Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite"
      ],
      "description": "Fate/stay night UBW 2nd Season — 2015 Spring",
      "metadata": {
        "year": 2015,
        "season": "Spring",
        "title_jp": "Fate/stay night [Unlimited Blade Works] 2nd Season",
        "title_en": "Fate/stay night UBW 2nd Season",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Fate/stay night UBW 2nd Season\nJapanese title: Fate/stay night [Unlimited Blade Works] 2nd Season\nYear: 2015\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/stay night UBW 2nd Season — 2015 Spring — favourite."
      ]
    },
    {
      "id": "anime:2015-spring-nisekoi-f5425b4e6d",
      "source_type": "anime",
      "title": "Nisekoi:",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Nisekoi: — 2015 Spring",
      "metadata": {
        "year": 2015,
        "season": "Spring",
        "title_jp": "ニセコイ:",
        "title_en": "Nisekoi:",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nisekoi:\nJapanese title: ニセコイ:\nYear: 2015\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nisekoi: — 2015 Spring."
      ]
    },
    {
      "id": "anime:2015-spring-plastic-memories-7154735529",
      "source_type": "anime",
      "title": "Plastic Memories",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Plastic Memories — 2015 Spring",
      "metadata": {
        "year": 2015,
        "season": "Spring",
        "title_jp": "プラスティック・メモリーズ",
        "title_en": "Plastic Memories",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Plastic Memories\nJapanese title: プラスティック・メモリーズ\nYear: 2015\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Plastic Memories — 2015 Spring."
      ]
    },
    {
      "id": "anime:2015-spring-sound-euphonium-f5733a65dd",
      "source_type": "anime",
      "title": "Sound! Euphonium",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "Sound! Euphonium — 2015 Spring",
      "metadata": {
        "year": 2015,
        "season": "Spring",
        "title_jp": "響け！ユーフォニアム",
        "title_en": "Sound! Euphonium",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Sound! Euphonium\nJapanese title: 響け！ユーフォニアム\nYear: 2015\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Sound! Euphonium — 2015 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2015-winter-saekano-how-to-raise-a-boring-girlfriend-64a807d7c5",
      "source_type": "anime",
      "title": "Saekano: How to Raise a Boring Girlfriend",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "favourite",
        "visited"
      ],
      "description": "Saekano: How to Raise a Boring Girlfriend — 2015 Winter",
      "metadata": {
        "year": 2015,
        "season": "Winter",
        "title_jp": "冴えない彼女の育てかた",
        "title_en": "Saekano: How to Raise a Boring Girlfriend",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Saekano: How to Raise a Boring Girlfriend\nJapanese title: 冴えない彼女の育てかた\nYear: 2015\nSeason: Winter\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Saekano: How to Raise a Boring Girlfriend — 2015 Winter — favourite — location visited."
      ]
    },
    {
      "id": "anime:2015-winter-aldnoah-zero-part-2-489c2c0ad0",
      "source_type": "anime",
      "title": "Aldnoah.Zero Part 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Aldnoah.Zero Part 2 — 2015 Winter",
      "metadata": {
        "year": 2015,
        "season": "Winter",
        "title_jp": "アルドノア・ゼロ Part 2",
        "title_en": "Aldnoah.Zero Part 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Aldnoah.Zero Part 2\nJapanese title: アルドノア・ゼロ Part 2\nYear: 2015\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Aldnoah.Zero Part 2 — 2015 Winter."
      ]
    },
    {
      "id": "anime:2015-winter-td-7b3d1a0385",
      "source_type": "anime",
      "title": "侦探歌剧 少女福尔摩斯 TD",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2015",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "侦探歌剧 少女福尔摩斯 TD — 2015 Winter",
      "metadata": {
        "year": 2015,
        "season": "Winter",
        "title_jp": "探偵歌劇 ミルキィホームズ TD",
        "title_en": "侦探歌剧 少女福尔摩斯 TD",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 侦探歌剧 少女福尔摩斯 TD\nJapanese title: 探偵歌劇 ミルキィホームズ TD\nYear: 2015\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "侦探歌剧 少女福尔摩斯 TD — 2015 Winter."
      ]
    },
    {
      "id": "anime:2014-film-tamako-love-story-51c843f96f",
      "source_type": "anime",
      "title": "Tamako Love Story",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "visited"
      ],
      "description": "Tamako Love Story — 2014 Film",
      "metadata": {
        "year": 2014,
        "season": "Film",
        "title_jp": "たまこラブストーリー",
        "title_en": "Tamako Love Story",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Tamako Love Story\nJapanese title: たまこラブストーリー\nYear: 2014\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Tamako Love Story — 2014 Film — location visited."
      ]
    },
    {
      "id": "anime:2014-fall-shirobako-a9a78c3fd3",
      "source_type": "anime",
      "title": "SHIROBAKO",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "SHIROBAKO — 2014 Fall",
      "metadata": {
        "year": 2014,
        "season": "Fall",
        "title_jp": "SHIROBAKO",
        "title_en": "SHIROBAKO",
        "starred": true,
        "seichi": true
      },
      "content": "English title: SHIROBAKO\nJapanese title: SHIROBAKO\nYear: 2014\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "SHIROBAKO — 2014 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2014-fall-your-lie-in-april-22dc4273bc",
      "source_type": "anime",
      "title": "Your Lie in April",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Your Lie in April — 2014 Fall",
      "metadata": {
        "year": 2014,
        "season": "Fall",
        "title_jp": "四月は君の嘘",
        "title_en": "Your Lie in April",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Your Lie in April\nJapanese title: 四月は君の嘘\nYear: 2014\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Your Lie in April — 2014 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2014-fall-fate-stay-night-unlimited-blade-works-c07abb2e9b",
      "source_type": "anime",
      "title": "Fate/stay night [Unlimited Blade Works]",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "Fate/stay night [Unlimited Blade Works] — 2014 Fall",
      "metadata": {
        "year": 2014,
        "season": "Fall",
        "title_jp": "Fate/stay night [Unlimited Blade Works]",
        "title_en": "Fate/stay night [Unlimited Blade Works]",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Fate/stay night [Unlimited Blade Works]\nJapanese title: Fate/stay night [Unlimited Blade Works]\nYear: 2014\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/stay night [Unlimited Blade Works] — 2014 Fall — favourite."
      ]
    },
    {
      "id": "anime:2014-fall-psycho-pass-2-a71b5575d9",
      "source_type": "anime",
      "title": "PSYCHO-PASS 心靈判官 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "PSYCHO-PASS 心靈判官 2 — 2014 Fall",
      "metadata": {
        "year": 2014,
        "season": "Fall",
        "title_jp": "PSYCHO-PASS サイコパス 2",
        "title_en": "PSYCHO-PASS 心靈判官 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: PSYCHO-PASS 心靈判官 2\nJapanese title: PSYCHO-PASS サイコパス 2\nYear: 2014\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "PSYCHO-PASS 心靈判官 2 — 2014 Fall."
      ]
    },
    {
      "id": "anime:2014-summer-aldnoah-zero-f1a0149aec",
      "source_type": "anime",
      "title": "Aldnoah.Zero (爛尾)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Aldnoah.Zero (爛尾) — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "アルドノア・ゼロ",
        "title_en": "Aldnoah.Zero (爛尾)",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Aldnoah.Zero (爛尾)\nJapanese title: アルドノア・ゼロ\nYear: 2014\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Aldnoah.Zero (爛尾) — 2014 Summer."
      ]
    },
    {
      "id": "anime:2014-summer-blade-dance-of-the-elementalers-385371cf12",
      "source_type": "anime",
      "title": "Blade Dance of the Elementalers",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Blade Dance of the Elementalers — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "精霊使いの剣舞",
        "title_en": "Blade Dance of the Elementalers",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Blade Dance of the Elementalers\nJapanese title: 精霊使いの剣舞\nYear: 2014\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Blade Dance of the Elementalers — 2014 Summer."
      ]
    },
    {
      "id": "anime:2014-summer-glasslip-aug-2014-f57b4e75af",
      "source_type": "anime",
      "title": "Glasslip (聖地巡礼: 福井, Aug 2014)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite",
        "visited"
      ],
      "description": "Glasslip (聖地巡礼: 福井, Aug 2014) — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "GLASSLIP",
        "title_en": "Glasslip (聖地巡礼: 福井, Aug 2014)",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Glasslip (聖地巡礼: 福井, Aug 2014)\nJapanese title: GLASSLIP\nYear: 2014\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Glasslip (聖地巡礼: 福井, Aug 2014) — 2014 Summer — favourite — location visited."
      ]
    },
    {
      "id": "anime:2014-summer-fate-kaleid-liner-prisma-illya-2wei-a60e2cf99e",
      "source_type": "anime",
      "title": "Fate/kaleid liner Prisma Illya 2wei",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Fate/kaleid liner Prisma Illya 2wei — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "Fate/kaleid liner プリズマ☆イリヤ ツヴァイ!",
        "title_en": "Fate/kaleid liner Prisma Illya 2wei",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/kaleid liner Prisma Illya 2wei\nJapanese title: Fate/kaleid liner プリズマ☆イリヤ ツヴァイ!\nYear: 2014\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/kaleid liner Prisma Illya 2wei — 2014 Summer."
      ]
    },
    {
      "id": "anime:2014-summer-glasslip-7abc22022e",
      "source_type": "anime",
      "title": "GLASSLIP",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite"
      ],
      "description": "GLASSLIP — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "グラスリップ",
        "title_en": "GLASSLIP",
        "starred": true,
        "seichi": false,
        "starred2": true
      },
      "content": "English title: GLASSLIP\nJapanese title: グラスリップ\nYear: 2014\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "GLASSLIP — 2014 Summer — favourite."
      ]
    },
    {
      "id": "anime:2014-summer-de8fc9e962a7-53d26991ba",
      "source_type": "anime",
      "title": "刀劍神域Ⅱ",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite"
      ],
      "description": "刀劍神域Ⅱ — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "ソードアート・オンラインⅡ",
        "title_en": "刀劍神域Ⅱ",
        "starred": true,
        "seichi": false
      },
      "content": "English title: 刀劍神域Ⅱ\nJapanese title: ソードアート・オンラインⅡ\nYear: 2014\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域Ⅱ — 2014 Summer — favourite."
      ]
    },
    {
      "id": "anime:2014-summer-f90389c4f025-88e623b55e",
      "source_type": "anime",
      "title": "前進吧！登山少女 第二季",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite",
        "visited"
      ],
      "description": "前進吧！登山少女 第二季 — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "ヤマノススメ セカンドシーズン",
        "title_en": "前進吧！登山少女 第二季",
        "starred": true,
        "seichi": true
      },
      "content": "English title: 前進吧！登山少女 第二季\nJapanese title: ヤマノススメ セカンドシーズン\nYear: 2014\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "前進吧！登山少女 第二季 — 2014 Summer — favourite — location visited."
      ]
    },
    {
      "id": "anime:2014-summer-c020c4df9bd7-034acebb99",
      "source_type": "anime",
      "title": "花物語",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "花物語 — 2014 Summer",
      "metadata": {
        "year": 2014,
        "season": "Summer",
        "title_jp": "花物語",
        "title_en": "花物語",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 花物語\nJapanese title: 花物語\nYear: 2014\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "花物語 — 2014 Summer."
      ]
    },
    {
      "id": "anime:2014-spring-the-irregular-at-magic-high-school-d0d0affa35",
      "source_type": "anime",
      "title": "The Irregular at Magic High School",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "The Irregular at Magic High School — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "魔法科高校の劣等生",
        "title_en": "The Irregular at Magic High School",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Irregular at Magic High School\nJapanese title: 魔法科高校の劣等生\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Irregular at Magic High School — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-spring-the-kawai-complex-41367edbc2",
      "source_type": "anime",
      "title": "The Kawai Complex",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "The Kawai Complex — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "僕らはみんな河合荘",
        "title_en": "The Kawai Complex",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Kawai Complex\nJapanese title: 僕らはみんな河合荘\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Kawai Complex — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-spring-one-week-friends-c48011496a",
      "source_type": "anime",
      "title": "One Week Friends",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "One Week Friends — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "一週間フレンズ。",
        "title_en": "One Week Friends",
        "starred": false,
        "seichi": false
      },
      "content": "English title: One Week Friends\nJapanese title: 一週間フレンズ。\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "One Week Friends — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-spring-is-the-order-a-rabbit-f1cfc374d0",
      "source_type": "anime",
      "title": "Is the Order a Rabbit?",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Is the Order a Rabbit? — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "ご注文はうさぎですか？",
        "title_en": "Is the Order a Rabbit?",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Is the Order a Rabbit?\nJapanese title: ご注文はうさぎですか？\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Is the Order a Rabbit? — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-spring-lovelive-589898659e",
      "source_type": "anime",
      "title": "LoveLive!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "LoveLive! — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "ラブライブ！",
        "title_en": "LoveLive!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: LoveLive!\nJapanese title: ラブライブ！\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "LoveLive! — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-spring-182d5d4dd9bc-7326621a35",
      "source_type": "anime",
      "title": "約會大作戰Ⅱ",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "約會大作戰Ⅱ — 2014 Spring",
      "metadata": {
        "year": 2014,
        "season": "Spring",
        "title_jp": "デート・ア・ライブⅡ",
        "title_en": "約會大作戰Ⅱ",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 約會大作戰Ⅱ\nJapanese title: デート・ア・ライブⅡ\nYear: 2014\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "約會大作戰Ⅱ — 2014 Spring."
      ]
    },
    {
      "id": "anime:2014-winter-chunibyo-s2-0b98a45253",
      "source_type": "anime",
      "title": "Chunibyo S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Chunibyo S2 — 2014 Winter",
      "metadata": {
        "year": 2014,
        "season": "Winter",
        "title_jp": "中二病でも恋がしたい！戀",
        "title_en": "Chunibyo S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Chunibyo S2\nJapanese title: 中二病でも恋がしたい！戀\nYear: 2014\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Chunibyo S2 — 2014 Winter."
      ]
    },
    {
      "id": "anime:2014-winter-seitokai-yakuindomo-s2-fe53af9727",
      "source_type": "anime",
      "title": "Seitokai Yakuindomo S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Seitokai Yakuindomo S2 — 2014 Winter",
      "metadata": {
        "year": 2014,
        "season": "Winter",
        "title_jp": "生徒会役員共＊",
        "title_en": "Seitokai Yakuindomo S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Seitokai Yakuindomo S2\nJapanese title: 生徒会役員共＊\nYear: 2014\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Seitokai Yakuindomo S2 — 2014 Winter."
      ]
    },
    {
      "id": "anime:2014-winter-nourin-gifu-2014-summer-trip-2144dce1f4",
      "source_type": "anime",
      "title": "Nourin (聖地巡礼: Gifu, 2014 summer trip)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "favourite",
        "visited"
      ],
      "description": "Nourin (聖地巡礼: Gifu, 2014 summer trip) — 2014 Winter",
      "metadata": {
        "year": 2014,
        "season": "Winter",
        "title_jp": "のうりん",
        "title_en": "Nourin (聖地巡礼: Gifu, 2014 summer trip)",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Nourin (聖地巡礼: Gifu, 2014 summer trip)\nJapanese title: のうりん\nYear: 2014\nSeason: Winter\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Nourin (聖地巡礼: Gifu, 2014 summer trip) — 2014 Winter — favourite — location visited."
      ]
    },
    {
      "id": "anime:2014-winter-nisekoi-275279d9d1",
      "source_type": "anime",
      "title": "Nisekoi",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Nisekoi — 2014 Winter",
      "metadata": {
        "year": 2014,
        "season": "Winter",
        "title_jp": "ニセコイ",
        "title_en": "Nisekoi",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nisekoi\nJapanese title: ニセコイ\nYear: 2014\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nisekoi — 2014 Winter."
      ]
    },
    {
      "id": "anime:2014-winter-33a079ce8f4c-40678b9d53",
      "source_type": "anime",
      "title": "鄰座同學是怪咖",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2014",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "鄰座同學是怪咖 — 2014 Winter",
      "metadata": {
        "year": 2014,
        "season": "Winter",
        "title_jp": "となりの関くん",
        "title_en": "鄰座同學是怪咖",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 鄰座同學是怪咖\nJapanese title: となりの関くん\nYear: 2014\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "鄰座同學是怪咖 — 2014 Winter."
      ]
    },
    {
      "id": "anime:2013-ova-the-pet-girl-of-sakurasou-ova-af1b2b778e",
      "source_type": "anime",
      "title": "The Pet Girl of Sakurasou OVA",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova"
      ],
      "description": "The Pet Girl of Sakurasou OVA — 2013 OVA",
      "metadata": {
        "year": 2013,
        "season": "OVA",
        "title_jp": "さくら荘のペットな彼女 OVA",
        "title_en": "The Pet Girl of Sakurasou OVA",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Pet Girl of Sakurasou OVA\nJapanese title: さくら荘のペットな彼女 OVA\nYear: 2013\nSeason: OVA\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Pet Girl of Sakurasou OVA — 2013 OVA."
      ]
    },
    {
      "id": "anime:2013-film-madoka-magica-rebellion-81891d9265",
      "source_type": "anime",
      "title": "Madoka Magica: Rebellion",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "Madoka Magica: Rebellion — 2013 Film",
      "metadata": {
        "year": 2013,
        "season": "Film",
        "title_jp": "劇場版 魔法少女まどか☆マギカ [新編] 叛逆の物語",
        "title_en": "Madoka Magica: Rebellion",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Madoka Magica: Rebellion\nJapanese title: 劇場版 魔法少女まどか☆マギカ [新編] 叛逆の物語\nYear: 2013\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Madoka Magica: Rebellion — 2013 Film."
      ]
    },
    {
      "id": "anime:2013-film-hanasaku-iroha-home-sweet-home-2b3ce0eb78",
      "source_type": "anime",
      "title": "Hanasaku Iroha: Home Sweet Home",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "Hanasaku Iroha: Home Sweet Home — 2013 Film",
      "metadata": {
        "year": 2013,
        "season": "Film",
        "title_jp": "劇場版 花咲くいろは HOME SWEET HOME",
        "title_en": "Hanasaku Iroha: Home Sweet Home",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Hanasaku Iroha: Home Sweet Home\nJapanese title: 劇場版 花咲くいろは HOME SWEET HOME\nYear: 2013\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Hanasaku Iroha: Home Sweet Home — 2013 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2013-fall-beyond-the-boundary-a11577afe2",
      "source_type": "anime",
      "title": "Beyond the Boundary",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Beyond the Boundary — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "境界の彼方",
        "title_en": "Beyond the Boundary",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Beyond the Boundary\nJapanese title: 境界の彼方\nYear: 2013\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Beyond the Boundary — 2013 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2013-fall-strike-the-blood-269167b74a",
      "source_type": "anime",
      "title": "Strike the Blood",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Strike the Blood — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "ストライク・ザ・ブラッド",
        "title_en": "Strike the Blood",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Strike the Blood\nJapanese title: ストライク・ザ・ブラッド\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Strike the Blood — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-nagi-asu-a-lull-in-the-sea-5e1015564e",
      "source_type": "anime",
      "title": "Nagi-Asu: A Lull in the Sea",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Nagi-Asu: A Lull in the Sea — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "凪のあすから",
        "title_en": "Nagi-Asu: A Lull in the Sea",
        "starred": true,
        "seichi": true,
        "starred2": true
      },
      "content": "English title: Nagi-Asu: A Lull in the Sea\nJapanese title: 凪のあすから\nYear: 2013\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Nagi-Asu: A Lull in the Sea — 2013 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2013-fall-non-non-biyori-cf21466111",
      "source_type": "anime",
      "title": "Non Non Biyori",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Non Non Biyori — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "のんのんびより",
        "title_en": "Non Non Biyori",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Non Non Biyori\nJapanese title: のんのんびより\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Non Non Biyori — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-infinite-stratos-2-16b801d598",
      "source_type": "anime",
      "title": "Infinite Stratos 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Infinite Stratos 2 — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "IS〈インフィニット・ストラトス〉2",
        "title_en": "Infinite Stratos 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Infinite Stratos 2\nJapanese title: IS〈インフィニット・ストラトス〉2\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Infinite Stratos 2 — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-unbreakable-machine-doll-ac4d0520bc",
      "source_type": "anime",
      "title": "Unbreakable Machine-Doll",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Unbreakable Machine-Doll — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "機巧少女は傷つかない",
        "title_en": "Unbreakable Machine-Doll",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Unbreakable Machine-Doll\nJapanese title: 機巧少女は傷つかない\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Unbreakable Machine-Doll — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-white-album-2-ffa71ddd96",
      "source_type": "anime",
      "title": "White Album 2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "White Album 2 — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "WHITE ALBUM 2",
        "title_en": "White Album 2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: White Album 2\nJapanese title: WHITE ALBUM 2\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "White Album 2 — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-little-busters-refrain-6f822a7fb0",
      "source_type": "anime",
      "title": "Little Busters！~Refrain~",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Little Busters！~Refrain~ — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "リトルバスターズ！~Refrain~",
        "title_en": "Little Busters！~Refrain~",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Little Busters！~Refrain~\nJapanese title: リトルバスターズ！~Refrain~\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Little Busters！~Refrain~ — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-2a8d5840435c-1e150b1006",
      "source_type": "anime",
      "title": "我的腦內戀礙選項",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "我的腦內戀礙選項 — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "俺の脳内選択肢が、学園ラブコメを全力で邪魔している",
        "title_en": "我的腦內戀礙選項",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 我的腦內戀礙選項\nJapanese title: 俺の脳内選択肢が、学園ラブコメを全力で邪魔している\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "我的腦內戀礙選項 — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-fall-extra-edition-2dcdc80391",
      "source_type": "anime",
      "title": "刀劍神域 Extra Edition",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "刀劍神域 Extra Edition — 2013 Fall",
      "metadata": {
        "year": 2013,
        "season": "Fall",
        "title_jp": "ソードアート・オンライン Extra Edition",
        "title_en": "刀劍神域 Extra Edition",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 刀劍神域 Extra Edition\nJapanese title: ソードアート・オンライン Extra Edition\nYear: 2013\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "刀劍神域 Extra Edition — 2013 Fall."
      ]
    },
    {
      "id": "anime:2013-summer-monogatari-series-second-season-b972aea84b",
      "source_type": "anime",
      "title": "Monogatari Series: Second Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Monogatari Series: Second Season — 2013 Summer",
      "metadata": {
        "year": 2013,
        "season": "Summer",
        "title_jp": "〈物語〉シリーズ セカンドシーズン",
        "title_en": "Monogatari Series: Second Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Monogatari Series: Second Season\nJapanese title: 〈物語〉シリーズ セカンドシーズン\nYear: 2013\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Monogatari Series: Second Season — 2013 Summer."
      ]
    },
    {
      "id": "anime:2013-summer-fate-kaleid-liner-prisma-illya-b18a76e3bd",
      "source_type": "anime",
      "title": "Fate/kaleid liner Prisma Illya",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Fate/kaleid liner Prisma Illya — 2013 Summer",
      "metadata": {
        "year": 2013,
        "season": "Summer",
        "title_jp": "Fate/kaleid liner プリズマ☆イリヤ",
        "title_en": "Fate/kaleid liner Prisma Illya",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/kaleid liner Prisma Illya\nJapanese title: Fate/kaleid liner プリズマ☆イリヤ\nYear: 2013\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/kaleid liner Prisma Illya — 2013 Summer."
      ]
    },
    {
      "id": "anime:2013-summer-the-world-god-only-knows-goddesses-1404764884",
      "source_type": "anime",
      "title": "The World God Only Knows: Goddesses",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "The World God Only Knows: Goddesses — 2013 Summer",
      "metadata": {
        "year": 2013,
        "season": "Summer",
        "title_jp": "神のみぞ知るセカイ 女神篇",
        "title_en": "The World God Only Knows: Goddesses",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The World God Only Knows: Goddesses\nJapanese title: 神のみぞ知るセカイ 女神篇\nYear: 2013\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The World God Only Knows: Goddesses — 2013 Summer."
      ]
    },
    {
      "id": "anime:2013-summer-ss-fd43f7c849",
      "source_type": "anime",
      "title": "蘿球社！SS",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "蘿球社！SS — 2013 Summer",
      "metadata": {
        "year": 2013,
        "season": "Summer",
        "title_jp": "ロウきゅーぶ！SS",
        "title_en": "蘿球社！SS",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 蘿球社！SS\nJapanese title: ロウきゅーぶ！SS\nYear: 2013\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "蘿球社！SS — 2013 Summer."
      ]
    },
    {
      "id": "anime:2013-spring-attack-on-titan-aab4f4960c",
      "source_type": "anime",
      "title": "Attack on Titan",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Attack on Titan — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "進撃の巨人",
        "title_en": "Attack on Titan",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Attack on Titan\nJapanese title: 進撃の巨人\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Attack on Titan — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-spring-my-teen-romantic-comedy-snafu-af77a067db",
      "source_type": "anime",
      "title": "My Teen Romantic Comedy SNAFU",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "visited"
      ],
      "description": "My Teen Romantic Comedy SNAFU — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "やはり俺の青春ラブコメはまちがっている",
        "title_en": "My Teen Romantic Comedy SNAFU",
        "starred": false,
        "seichi": true
      },
      "content": "English title: My Teen Romantic Comedy SNAFU\nJapanese title: やはり俺の青春ラブコメはまちがっている\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "My Teen Romantic Comedy SNAFU — 2013 Spring — location visited."
      ]
    },
    {
      "id": "anime:2013-spring-date-a-live-60edde838f",
      "source_type": "anime",
      "title": "Date A Live",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Date A Live — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "デート・ア・ライブ",
        "title_en": "Date A Live",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Date A Live\nJapanese title: デート・ア・ライブ\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Date A Live — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-spring-henneko-9326d1c3a8",
      "source_type": "anime",
      "title": "HenNeko",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "HenNeko — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "変態王子と笑わない猫。",
        "title_en": "HenNeko",
        "starred": false,
        "seichi": false
      },
      "content": "English title: HenNeko\nJapanese title: 変態王子と笑わない猫。\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "HenNeko — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-spring-a-certain-scientific-railgun-s-f9f6316fff",
      "source_type": "anime",
      "title": "A Certain Scientific Railgun S",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "A Certain Scientific Railgun S — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "とある科学の超電磁砲S",
        "title_en": "A Certain Scientific Railgun S",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Scientific Railgun S\nJapanese title: とある科学の超電磁砲S\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Scientific Railgun S — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-spring-nyaruko-san-w-de212ad25b",
      "source_type": "anime",
      "title": "Nyaruko-san W",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Nyaruko-san W — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "這いよれ！ニャル子さん W",
        "title_en": "Nyaruko-san W",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nyaruko-san W\nJapanese title: 這いよれ！ニャル子さん W\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nyaruko-san W — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-spring-4ea5415a5754-14ce95c77a",
      "source_type": "anime",
      "title": "我的妹妹哪有這麼可愛。",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "我的妹妹哪有這麼可愛。 — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "俺の妹がこんなに可愛いわけがない。",
        "title_en": "我的妹妹哪有這麼可愛。",
        "starred": true,
        "seichi": true
      },
      "content": "English title: 我的妹妹哪有這麼可愛。\nJapanese title: 俺の妹がこんなに可愛いわけがない。\nYear: 2013\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "我的妹妹哪有這麼可愛。 — 2013 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2013-spring-cuties-43adbec167",
      "source_type": "anime",
      "title": "旋風管家！Cuties",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "旋風管家！Cuties — 2013 Spring",
      "metadata": {
        "year": 2013,
        "season": "Spring",
        "title_jp": "ハヤテのごとく！Cuties",
        "title_en": "旋風管家！Cuties",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 旋風管家！Cuties\nJapanese title: ハヤテのごとく！Cuties\nYear: 2013\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "旋風管家！Cuties — 2013 Spring."
      ]
    },
    {
      "id": "anime:2013-winter-oreshura-bc63473608",
      "source_type": "anime",
      "title": "OreShura",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "OreShura — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "俺の彼女と幼なじみが修羅場すぎる",
        "title_en": "OreShura",
        "starred": false,
        "seichi": false
      },
      "content": "English title: OreShura\nJapanese title: 俺の彼女と幼なじみが修羅場すぎる\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "OreShura — 2013 Winter."
      ]
    },
    {
      "id": "anime:2013-winter-tamako-market-f2b7288c2b",
      "source_type": "anime",
      "title": "Tamako Market",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "visited"
      ],
      "description": "Tamako Market — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "たまこまーけっと",
        "title_en": "Tamako Market",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Tamako Market\nJapanese title: たまこまーけっと\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Tamako Market — 2013 Winter — location visited."
      ]
    },
    {
      "id": "anime:2013-winter-haganai-next-885f0ba7a1",
      "source_type": "anime",
      "title": "Haganai NEXT",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Haganai NEXT — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "僕は友達が少ないNEXT",
        "title_en": "Haganai NEXT",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Haganai NEXT\nJapanese title: 僕は友達が少ないNEXT\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Haganai NEXT — 2013 Winter."
      ]
    },
    {
      "id": "anime:2013-winter-encouragement-of-climb-ef0d344392",
      "source_type": "anime",
      "title": "Encouragement of Climb",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "visited"
      ],
      "description": "Encouragement of Climb — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "ヤマノススメ",
        "title_en": "Encouragement of Climb",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Encouragement of Climb\nJapanese title: ヤマノススメ\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Encouragement of Climb — 2013 Winter — location visited."
      ]
    },
    {
      "id": "anime:2013-winter-iii-0785e97487",
      "source_type": "anime",
      "title": "初音島III",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "初音島III — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "D.C.III ~ダ・カーポIII~",
        "title_en": "初音島III",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 初音島III\nJapanese title: D.C.III ~ダ・カーポIII~\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "初音島III — 2013 Winter."
      ]
    },
    {
      "id": "anime:2013-winter-lovelive-4f5cfc2e92",
      "source_type": "anime",
      "title": "LoveLive!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter",
        "favourite",
        "visited"
      ],
      "description": "LoveLive! — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "ラブライブ！",
        "title_en": "LoveLive!",
        "starred": true,
        "seichi": true
      },
      "content": "English title: LoveLive!\nJapanese title: ラブライブ！\nYear: 2013\nSeason: Winter\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "LoveLive! — 2013 Winter — favourite — location visited."
      ]
    },
    {
      "id": "anime:2013-winter-lv-2-4522c1a7d7",
      "source_type": "anime",
      "title": "學生會的一存 Lv.2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2013",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "學生會的一存 Lv.2 — 2013 Winter",
      "metadata": {
        "year": 2013,
        "season": "Winter",
        "title_jp": "生徒会の一存 Lv.2",
        "title_en": "學生會的一存 Lv.2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 學生會的一存 Lv.2\nJapanese title: 生徒会の一存 Lv.2\nYear: 2013\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "學生會的一存 Lv.2 — 2013 Winter."
      ]
    },
    {
      "id": "anime:2012-fall-psycho-pass-0c0a638902",
      "source_type": "anime",
      "title": "PSYCHO-PASS",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "PSYCHO-PASS — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "PSYCHO-PASS",
        "title_en": "PSYCHO-PASS",
        "starred": false,
        "seichi": false
      },
      "content": "English title: PSYCHO-PASS\nJapanese title: PSYCHO-PASS\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "PSYCHO-PASS — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-love-chunibyo-other-delusions-2096ff74c6",
      "source_type": "anime",
      "title": "Love, Chunibyo & Other Delusions",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Love, Chunibyo & Other Delusions — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "中二病でも恋がしたい！",
        "title_en": "Love, Chunibyo & Other Delusions",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Love, Chunibyo & Other Delusions\nJapanese title: 中二病でも恋がしたい！\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Love, Chunibyo & Other Delusions — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-the-pet-girl-of-sakurasou-88c9803a09",
      "source_type": "anime",
      "title": "The Pet Girl of Sakurasou",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "The Pet Girl of Sakurasou — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "さくら荘のペットな彼女",
        "title_en": "The Pet Girl of Sakurasou",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Pet Girl of Sakurasou\nJapanese title: さくら荘のペットな彼女\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Pet Girl of Sakurasou — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-can-t-take-my-eyes-off-you-de6a9d92e3",
      "source_type": "anime",
      "title": "旋風管家! CAN'T TAKE MY EYES OFF YOU",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "旋風管家! CAN'T TAKE MY EYES OFF YOU — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "ハヤテのごとく! CAN'T TAKE MY EYES OFF YOU",
        "title_en": "旋風管家! CAN'T TAKE MY EYES OFF YOU",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 旋風管家! CAN'T TAKE MY EYES OFF YOU\nJapanese title: ハヤテのごとく! CAN'T TAKE MY EYES OFF YOU\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "旋風管家! CAN'T TAKE MY EYES OFF YOU — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-darkness-ca7952c995",
      "source_type": "anime",
      "title": "出包王女DARKNESS",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "出包王女DARKNESS — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "To LOVEる -とらぶる- ダークネス-",
        "title_en": "出包王女DARKNESS",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 出包王女DARKNESS\nJapanese title: To LOVEる -とらぶる- ダークネス-\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "出包王女DARKNESS — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-little-busters-0a61130a6a",
      "source_type": "anime",
      "title": "Little Busters!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Little Busters! — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "リトルバスターズ！",
        "title_en": "Little Busters!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Little Busters!\nJapanese title: リトルバスターズ！\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Little Busters! — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-fall-aa66ff6c1662-029a210457",
      "source_type": "anime",
      "title": "貓物語（黑）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "貓物語（黑） — 2012 Fall",
      "metadata": {
        "year": 2012,
        "season": "Fall",
        "title_jp": "猫物語（黒）",
        "title_en": "貓物語（黑）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 貓物語（黑）\nJapanese title: 猫物語（黒）\nYear: 2012\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "貓物語（黑） — 2012 Fall."
      ]
    },
    {
      "id": "anime:2012-summer-sword-art-online-e33842559d",
      "source_type": "anime",
      "title": "Sword Art Online",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite"
      ],
      "description": "Sword Art Online — 2012 Summer",
      "metadata": {
        "year": 2012,
        "season": "Summer",
        "title_jp": "ソードアート・オンライン",
        "title_en": "Sword Art Online",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Sword Art Online\nJapanese title: ソードアート・オンライン\nYear: 2012\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Sword Art Online — 2012 Summer — favourite."
      ]
    },
    {
      "id": "anime:2012-summer-tari-tari-76eeec2dee",
      "source_type": "anime",
      "title": "Tari Tari",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer",
        "favourite",
        "visited"
      ],
      "description": "Tari Tari — 2012 Summer",
      "metadata": {
        "year": 2012,
        "season": "Summer",
        "title_jp": "Tari Tari",
        "title_en": "Tari Tari",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Tari Tari\nJapanese title: Tari Tari\nYear: 2012\nSeason: Summer\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Tari Tari — 2012 Summer — favourite — location visited."
      ]
    },
    {
      "id": "anime:2012-spring-hyouka-0f7fb725b6",
      "source_type": "anime",
      "title": "Hyouka",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "visited"
      ],
      "description": "Hyouka — 2012 Spring",
      "metadata": {
        "year": 2012,
        "season": "Spring",
        "title_jp": "氷菓",
        "title_en": "Hyouka",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Hyouka\nJapanese title: 氷菓\nYear: 2012\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Hyouka — 2012 Spring — location visited."
      ]
    },
    {
      "id": "anime:2012-spring-fate-zero-2nd-season-b10cc0364b",
      "source_type": "anime",
      "title": "Fate/Zero 2nd Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Fate/Zero 2nd Season — 2012 Spring",
      "metadata": {
        "year": 2012,
        "season": "Spring",
        "title_jp": "Fate/Zero 2nd Season",
        "title_en": "Fate/Zero 2nd Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/Zero 2nd Season\nJapanese title: Fate/Zero 2nd Season\nYear: 2012\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/Zero 2nd Season — 2012 Spring."
      ]
    },
    {
      "id": "anime:2012-spring-accel-world-828a972830",
      "source_type": "anime",
      "title": "Accel World",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite"
      ],
      "description": "Accel World — 2012 Spring",
      "metadata": {
        "year": 2012,
        "season": "Spring",
        "title_jp": "アクセル・ワールド",
        "title_en": "Accel World",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Accel World\nJapanese title: アクセル・ワールド\nYear: 2012\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Accel World — 2012 Spring — favourite."
      ]
    },
    {
      "id": "anime:2012-spring-nyaruko-crawling-with-love-d009a42246",
      "source_type": "anime",
      "title": "Nyaruko: Crawling with Love",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Nyaruko: Crawling with Love — 2012 Spring",
      "metadata": {
        "year": 2012,
        "season": "Spring",
        "title_jp": "這いよれ！ニャル子さん",
        "title_en": "Nyaruko: Crawling with Love",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nyaruko: Crawling with Love\nJapanese title: 這いよれ！ニャル子さん\nYear: 2012\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nyaruko: Crawling with Love — 2012 Spring."
      ]
    },
    {
      "id": "anime:2012-spring-b144ea982081-36a9fda3d9",
      "source_type": "anime",
      "title": "夏色奇蹟",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "夏色奇蹟 — 2012 Spring",
      "metadata": {
        "year": 2012,
        "season": "Spring",
        "title_jp": "夏色キセキ",
        "title_en": "夏色奇蹟",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 夏色奇蹟\nJapanese title: 夏色キセキ\nYear: 2012\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "夏色奇蹟 — 2012 Spring."
      ]
    },
    {
      "id": "anime:2012-winter-nisemonogatari-47394b86cf",
      "source_type": "anime",
      "title": "Nisemonogatari",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Nisemonogatari — 2012 Winter",
      "metadata": {
        "year": 2012,
        "season": "Winter",
        "title_jp": "偽物語",
        "title_en": "Nisemonogatari",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nisemonogatari\nJapanese title: 偽物語\nYear: 2012\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nisemonogatari — 2012 Winter."
      ]
    },
    {
      "id": "anime:2012-winter-the-familiar-of-zero-f-97ae72115c",
      "source_type": "anime",
      "title": "The Familiar of Zero F",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "The Familiar of Zero F — 2012 Winter",
      "metadata": {
        "year": 2012,
        "season": "Winter",
        "title_jp": "ゼロの使い魔F",
        "title_en": "The Familiar of Zero F",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Familiar of Zero F\nJapanese title: ゼロの使い魔F\nYear: 2012\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Familiar of Zero F — 2012 Winter."
      ]
    },
    {
      "id": "anime:2012-winter-a9c4f910c38a-3610886cea",
      "source_type": "anime",
      "title": "偵探歌劇 少女福爾摩斯 第二幕",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2012",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "偵探歌劇 少女福爾摩斯 第二幕 — 2012 Winter",
      "metadata": {
        "year": 2012,
        "season": "Winter",
        "title_jp": "探偵オペラ ミルキィホームズ 第二幕",
        "title_en": "偵探歌劇 少女福爾摩斯 第二幕",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 偵探歌劇 少女福爾摩斯 第二幕\nJapanese title: 探偵オペラ ミルキィホームズ 第二幕\nYear: 2012\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "偵探歌劇 少女福爾摩斯 第二幕 — 2012 Winter."
      ]
    },
    {
      "id": "anime:2011-film-k-on-movie-462d10e316",
      "source_type": "anime",
      "title": "K-On! Movie",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film",
        "favourite",
        "visited"
      ],
      "description": "K-On! Movie — 2011 Film",
      "metadata": {
        "year": 2011,
        "season": "Film",
        "title_jp": "劇場版 けいおん!",
        "title_en": "K-On! Movie",
        "starred": true,
        "seichi": true
      },
      "content": "English title: K-On! Movie\nJapanese title: 劇場版 けいおん!\nYear: 2011\nSeason: Film\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "K-On! Movie — 2011 Film — favourite — location visited."
      ]
    },
    {
      "id": "anime:2011-fall-fate-zero-fe78fef9bb",
      "source_type": "anime",
      "title": "Fate/Zero",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Fate/Zero — 2011 Fall",
      "metadata": {
        "year": 2011,
        "season": "Fall",
        "title_jp": "Fate/Zero",
        "title_en": "Fate/Zero",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/Zero\nJapanese title: Fate/Zero\nYear: 2011\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/Zero — 2011 Fall."
      ]
    },
    {
      "id": "anime:2011-fall-haganai-i-don-t-have-many-friends-6bb5153062",
      "source_type": "anime",
      "title": "Haganai: I Don't Have Many Friends",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Haganai: I Don't Have Many Friends — 2011 Fall",
      "metadata": {
        "year": 2011,
        "season": "Fall",
        "title_jp": "僕は友達が少ない",
        "title_en": "Haganai: I Don't Have Many Friends",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Haganai: I Don't Have Many Friends\nJapanese title: 僕は友達が少ない\nYear: 2011\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Haganai: I Don't Have Many Friends — 2011 Fall."
      ]
    },
    {
      "id": "anime:2011-fall-shakugan-no-shana-iii-final-a056195c67",
      "source_type": "anime",
      "title": "Shakugan no Shana III Final",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Shakugan no Shana III Final — 2011 Fall",
      "metadata": {
        "year": 2011,
        "season": "Fall",
        "title_jp": "灼眼のシャナIII Final",
        "title_en": "Shakugan no Shana III Final",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Shakugan no Shana III Final\nJapanese title: 灼眼のシャナIII Final\nYear: 2011\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Shakugan no Shana III Final — 2011 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2011-spring-anohana-the-flower-we-saw-that-day-68a5cee637",
      "source_type": "anime",
      "title": "AnoHana: The Flower We Saw That Day",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "AnoHana: The Flower We Saw That Day — 2011 Spring",
      "metadata": {
        "year": 2011,
        "season": "Spring",
        "title_jp": "あの日見た花の名前を僕達はまだ知らない",
        "title_en": "AnoHana: The Flower We Saw That Day",
        "starred": true,
        "seichi": true
      },
      "content": "English title: AnoHana: The Flower We Saw That Day\nJapanese title: あの日見た花の名前を僕達はまだ知らない\nYear: 2011\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "AnoHana: The Flower We Saw That Day — 2011 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2011-spring-hanasaku-iroha-64b9ff9d49",
      "source_type": "anime",
      "title": "Hanasaku Iroha",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite",
        "visited"
      ],
      "description": "Hanasaku Iroha — 2011 Spring",
      "metadata": {
        "year": 2011,
        "season": "Spring",
        "title_jp": "花咲くいろは",
        "title_en": "Hanasaku Iroha",
        "starred": true,
        "seichi": true,
        "starred2": true
      },
      "content": "English title: Hanasaku Iroha\nJapanese title: 花咲くいろは\nYear: 2011\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Hanasaku Iroha — 2011 Spring — favourite — location visited."
      ]
    },
    {
      "id": "anime:2011-spring-ground-control-to-psychoelectric-girl-689c4a999c",
      "source_type": "anime",
      "title": "Ground Control to Psychoelectric Girl",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Ground Control to Psychoelectric Girl — 2011 Spring",
      "metadata": {
        "year": 2011,
        "season": "Spring",
        "title_jp": "電波女と青春男",
        "title_en": "Ground Control to Psychoelectric Girl",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Ground Control to Psychoelectric Girl\nJapanese title: 電波女と青春男\nYear: 2011\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Ground Control to Psychoelectric Girl — 2011 Spring."
      ]
    },
    {
      "id": "anime:2011-spring-ii-9a38d54bc7",
      "source_type": "anime",
      "title": "只有神知道的世界II",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "只有神知道的世界II — 2011 Spring",
      "metadata": {
        "year": 2011,
        "season": "Spring",
        "title_jp": "神のみぞ知るセカイII",
        "title_en": "只有神知道的世界II",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 只有神知道的世界II\nJapanese title: 神のみぞ知るセカイII\nYear: 2011\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "只有神知道的世界II — 2011 Spring."
      ]
    },
    {
      "id": "anime:2011-winter-puella-magi-madoka-magica-cf8eb5f787",
      "source_type": "anime",
      "title": "Puella Magi Madoka Magica",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Puella Magi Madoka Magica — 2011 Winter",
      "metadata": {
        "year": 2011,
        "season": "Winter",
        "title_jp": "魔法少女まどか☆マギカ",
        "title_en": "Puella Magi Madoka Magica",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Puella Magi Madoka Magica\nJapanese title: 魔法少女まどか☆マギカ\nYear: 2011\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Puella Magi Madoka Magica — 2011 Winter."
      ]
    },
    {
      "id": "anime:2011-winter-infinite-stratos-51d056fca9",
      "source_type": "anime",
      "title": "Infinite Stratos",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2011",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Infinite Stratos — 2011 Winter",
      "metadata": {
        "year": 2011,
        "season": "Winter",
        "title_jp": "IS〈インフィニット・ストラトス〉",
        "title_en": "Infinite Stratos",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Infinite Stratos\nJapanese title: IS〈インフィニット・ストラトス〉\nYear: 2011\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Infinite Stratos — 2011 Winter."
      ]
    },
    {
      "id": "anime:2010-ova-gundam-unicorn-7-eps-4fde828ba6",
      "source_type": "anime",
      "title": "Gundam Unicorn (7 eps)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "ova",
        "favourite"
      ],
      "description": "Gundam Unicorn (7 eps) — 2010 OVA",
      "metadata": {
        "year": 2010,
        "season": "OVA",
        "title_jp": "機動戦士ガンダムUC",
        "title_en": "Gundam Unicorn (7 eps)",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Gundam Unicorn (7 eps)\nJapanese title: 機動戦士ガンダムUC\nYear: 2010\nSeason: OVA\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam Unicorn (7 eps) — 2010 OVA — favourite."
      ]
    },
    {
      "id": "anime:2010-film-the-disappearance-of-haruhi-suzumiya-551db1b616",
      "source_type": "anime",
      "title": "The Disappearance of Haruhi Suzumiya",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "film"
      ],
      "description": "The Disappearance of Haruhi Suzumiya — 2010 Film",
      "metadata": {
        "year": 2010,
        "season": "Film",
        "title_jp": "涼宮ハルヒの消失",
        "title_en": "The Disappearance of Haruhi Suzumiya",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Disappearance of Haruhi Suzumiya\nJapanese title: 涼宮ハルヒの消失\nYear: 2010\nSeason: Film\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Disappearance of Haruhi Suzumiya — 2010 Film."
      ]
    },
    {
      "id": "anime:2010-fall-oreimo-464a5227b0",
      "source_type": "anime",
      "title": "Oreimo",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Oreimo — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "俺の妹がこんなに可愛いわけがない。",
        "title_en": "Oreimo",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Oreimo\nJapanese title: 俺の妹がこんなに可愛いわけがない。\nYear: 2010\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Oreimo — 2010 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2010-fall-the-world-god-only-knows-7c841945ea",
      "source_type": "anime",
      "title": "The World God Only Knows",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "The World God Only Knows — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "神のみぞ知るセカイ",
        "title_en": "The World God Only Knows",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The World God Only Knows\nJapanese title: 神のみぞ知るセカイ\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The World God Only Knows — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-a-certain-magical-index-ii-1bc85dcc78",
      "source_type": "anime",
      "title": "A Certain Magical Index II",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "A Certain Magical Index II — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "とある魔術の禁書目録II",
        "title_en": "A Certain Magical Index II",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Magical Index II\nJapanese title: とある魔術の禁書目録II\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Magical Index II — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-motto-to-love-ru-7d01380c80",
      "source_type": "anime",
      "title": "Motto To Love-Ru",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Motto To Love-Ru — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "もっと To LOVEる",
        "title_en": "Motto To Love-Ru",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Motto To Love-Ru\nJapanese title: もっと To LOVEる\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Motto To Love-Ru — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-heaven-s-lost-property-forte-6ba5543541",
      "source_type": "anime",
      "title": "Heaven's Lost Property Forte",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Heaven's Lost Property Forte — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "そらのおとしものf",
        "title_en": "Heaven's Lost Property Forte",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Heaven's Lost Property Forte\nJapanese title: そらのおとしものf\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Heaven's Lost Property Forte — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-yosuga-no-sora-605ce7d6c1",
      "source_type": "anime",
      "title": "Yosuga no Sora",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Yosuga no Sora — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "ヨスガノソラ",
        "title_en": "Yosuga no Sora",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Yosuga no Sora\nJapanese title: ヨスガノソラ\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Yosuga no Sora — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-714197bee13f-438818532c",
      "source_type": "anime",
      "title": "偵探歌劇 少女福爾摩斯",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "偵探歌劇 少女福爾摩斯 — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "探偵オペラ ミルキィホームズ",
        "title_en": "偵探歌劇 少女福爾摩斯",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 偵探歌劇 少女福爾摩斯\nJapanese title: 探偵オペラ ミルキィホームズ\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "偵探歌劇 少女福爾摩斯 — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-fall-remember-my-love-craft-23e712aa2d",
      "source_type": "anime",
      "title": "潜行吧！奈亚子 Remember My Love（craft先生）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "潜行吧！奈亚子 Remember My Love（craft先生） — 2010 Fall",
      "metadata": {
        "year": 2010,
        "season": "Fall",
        "title_jp": "這いよる! ニャルアニ リメンバー・マイ・ラブ（クラフト先生）",
        "title_en": "潜行吧！奈亚子 Remember My Love（craft先生）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 潜行吧！奈亚子 Remember My Love（craft先生）\nJapanese title: 這いよる! ニャルアニ リメンバー・マイ・ラブ（クラフト先生）\nYear: 2010\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "潜行吧！奈亚子 Remember My Love（craft先生） — 2010 Fall."
      ]
    },
    {
      "id": "anime:2010-summer-seitokai-yakuindomo-8d6d3c2dd8",
      "source_type": "anime",
      "title": "Seitokai Yakuindomo",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Seitokai Yakuindomo — 2010 Summer",
      "metadata": {
        "year": 2010,
        "season": "Summer",
        "title_jp": "生徒会役員共",
        "title_en": "Seitokai Yakuindomo",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Seitokai Yakuindomo\nJapanese title: 生徒会役員共\nYear: 2010\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Seitokai Yakuindomo — 2010 Summer."
      ]
    },
    {
      "id": "anime:2010-spring-angel-beats-1d599e5b3d",
      "source_type": "anime",
      "title": "Angel Beats!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Angel Beats! — 2010 Spring",
      "metadata": {
        "year": 2010,
        "season": "Spring",
        "title_jp": "Angel Beats!",
        "title_en": "Angel Beats!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Angel Beats!\nJapanese title: Angel Beats!\nYear: 2010\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Angel Beats! — 2010 Spring."
      ]
    },
    {
      "id": "anime:2010-spring-k-on-s2-81298733c1",
      "source_type": "anime",
      "title": "K-On!! S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring",
        "favourite"
      ],
      "description": "K-On!! S2 — 2010 Spring",
      "metadata": {
        "year": 2010,
        "season": "Spring",
        "title_jp": "けいおん!!",
        "title_en": "K-On!! S2",
        "starred": true,
        "seichi": false
      },
      "content": "English title: K-On!! S2\nJapanese title: けいおん!!\nYear: 2010\nSeason: Spring\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "K-On!! S2 — 2010 Spring — favourite."
      ]
    },
    {
      "id": "anime:2010-spring-kiss-sis-b86174f111",
      "source_type": "anime",
      "title": "Kiss×sis",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Kiss×sis — 2010 Spring",
      "metadata": {
        "year": 2010,
        "season": "Spring",
        "title_jp": "kiss×sis",
        "title_en": "Kiss×sis",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kiss×sis\nJapanese title: kiss×sis\nYear: 2010\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kiss×sis — 2010 Spring."
      ]
    },
    {
      "id": "anime:2010-spring-sd-gundam-bravebattlewarriors-0b6fb0a99c",
      "source_type": "anime",
      "title": "SD GUNDAM三國傳 BraveBattleWarriors",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2010",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "SD GUNDAM三國傳 BraveBattleWarriors — 2010 Spring",
      "metadata": {
        "year": 2010,
        "season": "Spring",
        "title_jp": "SDガンダム三国伝 BraveBattleWarriors",
        "title_en": "SD GUNDAM三國傳 BraveBattleWarriors",
        "starred": false,
        "seichi": false
      },
      "content": "English title: SD GUNDAM三國傳 BraveBattleWarriors\nJapanese title: SDガンダム三国伝 BraveBattleWarriors\nYear: 2010\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "SD GUNDAM三國傳 BraveBattleWarriors — 2010 Spring."
      ]
    },
    {
      "id": "anime:2009-fall-a-certain-scientific-railgun-b8eecb017d",
      "source_type": "anime",
      "title": "A Certain Scientific Railgun",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "A Certain Scientific Railgun — 2009 Fall",
      "metadata": {
        "year": 2009,
        "season": "Fall",
        "title_jp": "とある科学の超電磁砲",
        "title_en": "A Certain Scientific Railgun",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Scientific Railgun\nJapanese title: とある科学の超電磁砲\nYear: 2009\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Scientific Railgun — 2009 Fall."
      ]
    },
    {
      "id": "anime:2009-fall-seitokai-no-ichizon-02e18ea80e",
      "source_type": "anime",
      "title": "Seitokai no Ichizon",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Seitokai no Ichizon — 2009 Fall",
      "metadata": {
        "year": 2009,
        "season": "Fall",
        "title_jp": "生徒会の一存",
        "title_en": "Seitokai no Ichizon",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Seitokai no Ichizon\nJapanese title: 生徒会の一存\nYear: 2009\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Seitokai no Ichizon — 2009 Fall."
      ]
    },
    {
      "id": "anime:2009-fall-heaven-s-lost-property-a52eaf1c61",
      "source_type": "anime",
      "title": "Heaven's Lost Property",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Heaven's Lost Property — 2009 Fall",
      "metadata": {
        "year": 2009,
        "season": "Fall",
        "title_jp": "そらのおとしもの",
        "title_en": "Heaven's Lost Property",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Heaven's Lost Property\nJapanese title: そらのおとしもの\nYear: 2009\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Heaven's Lost Property — 2009 Fall."
      ]
    },
    {
      "id": "anime:2009-fall-purezza-e15d4eea99",
      "source_type": "anime",
      "title": "乃木坂春香的秘密Purezza",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "乃木坂春香的秘密Purezza — 2009 Fall",
      "metadata": {
        "year": 2009,
        "season": "Fall",
        "title_jp": "乃木坂春香の秘密 ぴゅあれっつぁ♪",
        "title_en": "乃木坂春香的秘密Purezza",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 乃木坂春香的秘密Purezza\nJapanese title: 乃木坂春香の秘密 ぴゅあれっつぁ♪\nYear: 2009\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "乃木坂春香的秘密Purezza — 2009 Fall."
      ]
    },
    {
      "id": "anime:2009-summer-bakemonogatari-6d7f7faa0e",
      "source_type": "anime",
      "title": "Bakemonogatari",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Bakemonogatari — 2009 Summer",
      "metadata": {
        "year": 2009,
        "season": "Summer",
        "title_jp": "化物語",
        "title_en": "Bakemonogatari",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Bakemonogatari\nJapanese title: 化物語\nYear: 2009\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Bakemonogatari — 2009 Summer."
      ]
    },
    {
      "id": "anime:2009-summer-9035555caa76-ad2009576a",
      "source_type": "anime",
      "title": "懺·絕望先生",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "懺·絕望先生 — 2009 Summer",
      "metadata": {
        "year": 2009,
        "season": "Summer",
        "title_jp": "【懺・】さよなら絶望先生",
        "title_en": "懺·絕望先生",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 懺·絕望先生\nJapanese title: 【懺・】さよなら絶望先生\nYear: 2009\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "懺·絕望先生 — 2009 Summer."
      ]
    },
    {
      "id": "anime:2009-spring-k-on-d20c669502",
      "source_type": "anime",
      "title": "K-On!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "K-On! — 2009 Spring",
      "metadata": {
        "year": 2009,
        "season": "Spring",
        "title_jp": "けいおん！",
        "title_en": "K-On!",
        "starred": false,
        "seichi": false
      },
      "content": "English title: K-On!\nJapanese title: けいおん！\nYear: 2009\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "K-On! — 2009 Spring."
      ]
    },
    {
      "id": "anime:2009-spring-the-melancholy-of-haruhi-suzumiya-2009-fd94bbcdeb",
      "source_type": "anime",
      "title": "The Melancholy of Haruhi Suzumiya (2009)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "The Melancholy of Haruhi Suzumiya (2009) — 2009 Spring",
      "metadata": {
        "year": 2009,
        "season": "Spring",
        "title_jp": "涼宮ハルヒの憂鬱 (2009)",
        "title_en": "The Melancholy of Haruhi Suzumiya (2009)",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Melancholy of Haruhi Suzumiya (2009)\nJapanese title: 涼宮ハルヒの憂鬱 (2009)\nYear: 2009\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Melancholy of Haruhi Suzumiya (2009) — 2009 Spring."
      ]
    },
    {
      "id": "anime:2009-spring-hayate-the-combat-butler-s2-92b2052ad5",
      "source_type": "anime",
      "title": "Hayate the Combat Butler!! S2",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2009",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Hayate the Combat Butler!! S2 — 2009 Spring",
      "metadata": {
        "year": 2009,
        "season": "Spring",
        "title_jp": "ハヤテのごとく!!",
        "title_en": "Hayate the Combat Butler!! S2",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Hayate the Combat Butler!! S2\nJapanese title: ハヤテのごとく!!\nYear: 2009\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Hayate the Combat Butler!! S2 — 2009 Spring."
      ]
    },
    {
      "id": "anime:2008-fall-toradora-4390946e90",
      "source_type": "anime",
      "title": "Toradora!",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "Toradora! — 2008 Fall",
      "metadata": {
        "year": 2008,
        "season": "Fall",
        "title_jp": "とらドラ！",
        "title_en": "Toradora!",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Toradora!\nJapanese title: とらドラ！\nYear: 2008\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Toradora! — 2008 Fall — favourite."
      ]
    },
    {
      "id": "anime:2008-fall-clannad-after-story-fb8d99750f",
      "source_type": "anime",
      "title": "Clannad: After Story",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite"
      ],
      "description": "Clannad: After Story — 2008 Fall",
      "metadata": {
        "year": 2008,
        "season": "Fall",
        "title_jp": "CLANNAD〜AFTER STORY〜",
        "title_en": "Clannad: After Story",
        "starred": true,
        "seichi": false
      },
      "content": "English title: Clannad: After Story\nJapanese title: CLANNAD〜AFTER STORY〜\nYear: 2008\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Clannad: After Story — 2008 Fall — favourite."
      ]
    },
    {
      "id": "anime:2008-fall-a-certain-magical-index-7a8b034445",
      "source_type": "anime",
      "title": "A Certain Magical Index",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "A Certain Magical Index — 2008 Fall",
      "metadata": {
        "year": 2008,
        "season": "Fall",
        "title_jp": "とある魔術の禁書目録",
        "title_en": "A Certain Magical Index",
        "starred": false,
        "seichi": false
      },
      "content": "English title: A Certain Magical Index\nJapanese title: とある魔術の禁書目録\nYear: 2008\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "A Certain Magical Index — 2008 Fall."
      ]
    },
    {
      "id": "anime:2008-fall-984c77d9502b-caef1bcf23",
      "source_type": "anime",
      "title": "染红的街道",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "染红的街道 — 2008 Fall",
      "metadata": {
        "year": 2008,
        "season": "Fall",
        "title_jp": "あかね色に染まる坂",
        "title_en": "染红的街道",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 染红的街道\nJapanese title: あかね色に染まる坂\nYear: 2008\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "染红的街道 — 2008 Fall."
      ]
    },
    {
      "id": "anime:2008-fall-00-2-9bdccd8693",
      "source_type": "anime",
      "title": "機動戰士高達00（第2期）",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "機動戰士高達00（第2期） — 2008 Fall",
      "metadata": {
        "year": 2008,
        "season": "Fall",
        "title_jp": "機動戦士ガンダム00（第2期）",
        "title_en": "機動戰士高達00（第2期）",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 機動戰士高達00（第2期）\nJapanese title: 機動戦士ガンダム00（第2期）\nYear: 2008\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "機動戰士高達00（第2期） — 2008 Fall."
      ]
    },
    {
      "id": "anime:2008-summer-nogizaka-haruka-no-himitsu-dbe8cc2fc6",
      "source_type": "anime",
      "title": "Nogizaka Haruka no Himitsu",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Nogizaka Haruka no Himitsu — 2008 Summer",
      "metadata": {
        "year": 2008,
        "season": "Summer",
        "title_jp": "乃木坂春香の秘密",
        "title_en": "Nogizaka Haruka no Himitsu",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Nogizaka Haruka no Himitsu\nJapanese title: 乃木坂春香の秘密\nYear: 2008\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Nogizaka Haruka no Himitsu — 2008 Summer."
      ]
    },
    {
      "id": "anime:2008-summer-f067c5ced8d9-48c9f897e2",
      "source_type": "anime",
      "title": "零之使魔 ～三美姬的輪舞～",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "零之使魔 ～三美姬的輪舞～ — 2008 Summer",
      "metadata": {
        "year": 2008,
        "season": "Summer",
        "title_jp": "ゼロの使い魔 ～三美姫の輪舞～",
        "title_en": "零之使魔 ～三美姬的輪舞～",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 零之使魔 ～三美姬的輪舞～\nJapanese title: ゼロの使い魔 ～三美姫の輪舞～\nYear: 2008\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "零之使魔 ～三美姬的輪舞～ — 2008 Summer."
      ]
    },
    {
      "id": "anime:2008-spring-to-love-ru-e290ed9635",
      "source_type": "anime",
      "title": "To Love-Ru",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "To Love-Ru — 2008 Spring",
      "metadata": {
        "year": 2008,
        "season": "Spring",
        "title_jp": "To LOVEる-とらぶる-",
        "title_en": "To Love-Ru",
        "starred": false,
        "seichi": false
      },
      "content": "English title: To Love-Ru\nJapanese title: To LOVEる-とらぶる-\nYear: 2008\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "To Love-Ru — 2008 Spring."
      ]
    },
    {
      "id": "anime:2008-spring-kanokon-003ef94cc8",
      "source_type": "anime",
      "title": "Kanokon",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Kanokon — 2008 Spring",
      "metadata": {
        "year": 2008,
        "season": "Spring",
        "title_jp": "かのこん",
        "title_en": "Kanokon",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kanokon\nJapanese title: かのこん\nYear: 2008\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kanokon — 2008 Spring."
      ]
    },
    {
      "id": "anime:2008-spring-ii-s-s-63b72b6373",
      "source_type": "anime",
      "title": "初音島II S.S.",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "初音島II S.S. — 2008 Spring",
      "metadata": {
        "year": 2008,
        "season": "Spring",
        "title_jp": "D.C.II S.S. ～ダ・カーポII セカンドシーズン～",
        "title_en": "初音島II S.S.",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 初音島II S.S.\nJapanese title: D.C.II S.S. ～ダ・カーポII セカンドシーズン～\nYear: 2008\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "初音島II S.S. — 2008 Spring."
      ]
    },
    {
      "id": "anime:2008-winter-true-tears-ba02523eba",
      "source_type": "anime",
      "title": "true tears",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "true tears — 2008 Winter",
      "metadata": {
        "year": 2008,
        "season": "Winter",
        "title_jp": "true tears",
        "title_en": "true tears",
        "starred": false,
        "seichi": false
      },
      "content": "English title: true tears\nJapanese title: true tears\nYear: 2008\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "true tears — 2008 Winter."
      ]
    },
    {
      "id": "anime:2008-winter-zoku-sayonara-zetsubou-sensei-e9d70901ea",
      "source_type": "anime",
      "title": "Zoku Sayonara Zetsubou Sensei",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Zoku Sayonara Zetsubou Sensei — 2008 Winter",
      "metadata": {
        "year": 2008,
        "season": "Winter",
        "title_jp": "続・さよなら絶望先生",
        "title_en": "Zoku Sayonara Zetsubou Sensei",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Zoku Sayonara Zetsubou Sensei\nJapanese title: 続・さよなら絶望先生\nYear: 2008\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Zoku Sayonara Zetsubou Sensei — 2008 Winter."
      ]
    },
    {
      "id": "anime:2008-winter-f9487f5c6af9-eade4838f9",
      "source_type": "anime",
      "title": "俗·絕望先生",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2008",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "俗·絕望先生 — 2008 Winter",
      "metadata": {
        "year": 2008,
        "season": "Winter",
        "title_jp": "【俗・】さよなら絶望先生",
        "title_en": "俗·絕望先生",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 俗·絕望先生\nJapanese title: 【俗・】さよなら絶望先生\nYear: 2008\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "俗·絕望先生 — 2008 Winter."
      ]
    },
    {
      "id": "anime:2007-fall-clannad-9f029571bb",
      "source_type": "anime",
      "title": "CLANNAD",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "CLANNAD — 2007 Fall",
      "metadata": {
        "year": 2007,
        "season": "Fall",
        "title_jp": "CLANNAD",
        "title_en": "CLANNAD",
        "starred": false,
        "seichi": false
      },
      "content": "English title: CLANNAD\nJapanese title: CLANNAD\nYear: 2007\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "CLANNAD — 2007 Fall."
      ]
    },
    {
      "id": "anime:2007-fall-mobile-suit-gundam-00-aea48cbce5",
      "source_type": "anime",
      "title": "Mobile Suit Gundam 00",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Mobile Suit Gundam 00 — 2007 Fall",
      "metadata": {
        "year": 2007,
        "season": "Fall",
        "title_jp": "機動戦士ガンダム00",
        "title_en": "Mobile Suit Gundam 00",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Mobile Suit Gundam 00\nJapanese title: 機動戦士ガンダム00\nYear: 2007\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Mobile Suit Gundam 00 — 2007 Fall."
      ]
    },
    {
      "id": "anime:2007-fall-shakugan-no-shana-ii-b01b3bc0bc",
      "source_type": "anime",
      "title": "Shakugan no Shana II",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "favourite",
        "visited"
      ],
      "description": "Shakugan no Shana II — 2007 Fall",
      "metadata": {
        "year": 2007,
        "season": "Fall",
        "title_jp": "灼眼のシャナII",
        "title_en": "Shakugan no Shana II",
        "starred": true,
        "seichi": true
      },
      "content": "English title: Shakugan no Shana II\nJapanese title: 灼眼のシャナII\nYear: 2007\nSeason: Fall\nFavourite: yes\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Shakugan no Shana II — 2007 Fall — favourite — location visited."
      ]
    },
    {
      "id": "anime:2007-fall-da-capo-ii-5b31b3980c",
      "source_type": "anime",
      "title": "Da Capo II",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Da Capo II — 2007 Fall",
      "metadata": {
        "year": 2007,
        "season": "Fall",
        "title_jp": "D.C.II ～ダ・カーポII～",
        "title_en": "Da Capo II",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Da Capo II\nJapanese title: D.C.II ～ダ・カーポII～\nYear: 2007\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Da Capo II — 2007 Fall."
      ]
    },
    {
      "id": "anime:2007-summer-sayonara-zetsubou-sensei-e1e0dec976",
      "source_type": "anime",
      "title": "Sayonara, Zetsubou-Sensei",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Sayonara, Zetsubou-Sensei — 2007 Summer",
      "metadata": {
        "year": 2007,
        "season": "Summer",
        "title_jp": "さよなら絶望先生",
        "title_en": "Sayonara, Zetsubou-Sensei",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Sayonara, Zetsubou-Sensei\nJapanese title: さよなら絶望先生\nYear: 2007\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Sayonara, Zetsubou-Sensei — 2007 Summer."
      ]
    },
    {
      "id": "anime:2007-summer-b424699c8597-d51119bfc2",
      "source_type": "anime",
      "title": "零之使魔～雙月的騎士～",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "零之使魔～雙月的騎士～ — 2007 Summer",
      "metadata": {
        "year": 2007,
        "season": "Summer",
        "title_jp": "ゼロの使い魔〜双月の騎士〜",
        "title_en": "零之使魔～雙月的騎士～",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 零之使魔～雙月的騎士～\nJapanese title: ゼロの使い魔〜双月の騎士〜\nYear: 2007\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "零之使魔～雙月的騎士～ — 2007 Summer."
      ]
    },
    {
      "id": "anime:2007-spring-lucky-star-9a68c34451",
      "source_type": "anime",
      "title": "Lucky Star",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Lucky Star — 2007 Spring",
      "metadata": {
        "year": 2007,
        "season": "Spring",
        "title_jp": "らき☆すた",
        "title_en": "Lucky Star",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Lucky Star\nJapanese title: らき☆すた\nYear: 2007\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Lucky Star — 2007 Spring."
      ]
    },
    {
      "id": "anime:2007-spring-hayate-the-combat-butler-d535eb7ee3",
      "source_type": "anime",
      "title": "Hayate the Combat Butler",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2007",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "Hayate the Combat Butler — 2007 Spring",
      "metadata": {
        "year": 2007,
        "season": "Spring",
        "title_jp": "ハヤテのごとく！",
        "title_en": "Hayate the Combat Butler",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Hayate the Combat Butler\nJapanese title: ハヤテのごとく！\nYear: 2007\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Hayate the Combat Butler — 2007 Spring."
      ]
    },
    {
      "id": "anime:2006-fall-kanon-2006-3c48534ceb",
      "source_type": "anime",
      "title": "Kanon (2006)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2006",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Kanon (2006) — 2006 Fall",
      "metadata": {
        "year": 2006,
        "season": "Fall",
        "title_jp": "Kanon",
        "title_en": "Kanon (2006)",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Kanon (2006)\nJapanese title: Kanon\nYear: 2006\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Kanon (2006) — 2006 Fall."
      ]
    },
    {
      "id": "anime:2006-fall-10b880b78b54-d1a63b5de8",
      "source_type": "anime",
      "title": "薔薇少女 ～序曲～",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2006",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "薔薇少女 ～序曲～ — 2006 Fall",
      "metadata": {
        "year": 2006,
        "season": "Fall",
        "title_jp": "ローゼンメイデン オーベルテューレ",
        "title_en": "薔薇少女 ～序曲～",
        "starred": false,
        "seichi": false
      },
      "content": "English title: 薔薇少女 ～序曲～\nJapanese title: ローゼンメイデン オーベルテューレ\nYear: 2006\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "薔薇少女 ～序曲～ — 2006 Fall."
      ]
    },
    {
      "id": "anime:2006-summer-the-familiar-of-zero-fdcd23761d",
      "source_type": "anime",
      "title": "The Familiar of Zero",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2006",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "The Familiar of Zero — 2006 Summer",
      "metadata": {
        "year": 2006,
        "season": "Summer",
        "title_jp": "ゼロの使い魔",
        "title_en": "The Familiar of Zero",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Familiar of Zero\nJapanese title: ゼロの使い魔\nYear: 2006\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Familiar of Zero — 2006 Summer."
      ]
    },
    {
      "id": "anime:2006-spring-the-melancholy-of-haruhi-suzumiya-793157e19f",
      "source_type": "anime",
      "title": "The Melancholy of Haruhi Suzumiya",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2006",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "spring"
      ],
      "description": "The Melancholy of Haruhi Suzumiya — 2006 Spring",
      "metadata": {
        "year": 2006,
        "season": "Spring",
        "title_jp": "涼宮ハルヒの憂鬱",
        "title_en": "The Melancholy of Haruhi Suzumiya",
        "starred": false,
        "seichi": false
      },
      "content": "English title: The Melancholy of Haruhi Suzumiya\nJapanese title: 涼宮ハルヒの憂鬱\nYear: 2006\nSeason: Spring\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "The Melancholy of Haruhi Suzumiya — 2006 Spring."
      ]
    },
    {
      "id": "anime:2006-winter-fate-stay-night-b1510a1461",
      "source_type": "anime",
      "title": "Fate/stay night",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2006",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "winter"
      ],
      "description": "Fate/stay night — 2006 Winter",
      "metadata": {
        "year": 2006,
        "season": "Winter",
        "title_jp": "Fate/stay night",
        "title_en": "Fate/stay night",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fate/stay night\nJapanese title: Fate/stay night\nYear: 2006\nSeason: Winter\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fate/stay night — 2006 Winter."
      ]
    },
    {
      "id": "anime:2005-fall-shakugan-no-shana-d45a4c3e82",
      "source_type": "anime",
      "title": "Shakugan no Shana",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2005",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall",
        "visited"
      ],
      "description": "Shakugan no Shana — 2005 Fall",
      "metadata": {
        "year": 2005,
        "season": "Fall",
        "title_jp": "灼眼のシャナ",
        "title_en": "Shakugan no Shana",
        "starred": false,
        "seichi": true
      },
      "content": "English title: Shakugan no Shana\nJapanese title: 灼眼のシャナ\nYear: 2005\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: yes",
      "excerpts": [
        "Shakugan no Shana — 2005 Fall — location visited."
      ]
    },
    {
      "id": "anime:2005-fall-rozen-maiden-tr-umend-ecbe799967",
      "source_type": "anime",
      "title": "Rozen Maiden: Träumend",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2005",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Rozen Maiden: Träumend — 2005 Fall",
      "metadata": {
        "year": 2005,
        "season": "Fall",
        "title_jp": "ローゼンメイデン トロイメント",
        "title_en": "Rozen Maiden: Träumend",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rozen Maiden: Träumend\nJapanese title: ローゼンメイデン トロイメント\nYear: 2005\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rozen Maiden: Träumend — 2005 Fall."
      ]
    },
    {
      "id": "anime:2005-summer-da-capo-second-season-e2f0bf1b7e",
      "source_type": "anime",
      "title": "Da Capo Second Season",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2005",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Da Capo Second Season — 2005 Summer",
      "metadata": {
        "year": 2005,
        "season": "Summer",
        "title_jp": "D.C.S.S. ～ダ・カーポ セカンドシーズン～",
        "title_en": "Da Capo Second Season",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Da Capo Second Season\nJapanese title: D.C.S.S. ～ダ・カーポ セカンドシーズン～\nYear: 2005\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Da Capo Second Season — 2005 Summer."
      ]
    },
    {
      "id": "anime:2004-fall-rozen-maiden-e4b0fe9e5e",
      "source_type": "anime",
      "title": "Rozen Maiden",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2004",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Rozen Maiden — 2004 Fall",
      "metadata": {
        "year": 2004,
        "season": "Fall",
        "title_jp": "ローゼンメイデン",
        "title_en": "Rozen Maiden",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Rozen Maiden\nJapanese title: ローゼンメイデン\nYear: 2004\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Rozen Maiden — 2004 Fall."
      ]
    },
    {
      "id": "anime:2004-fall-gundam-seed-destiny-61e21967cd",
      "source_type": "anime",
      "title": "Gundam SEED Destiny",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2004",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Gundam SEED Destiny — 2004 Fall",
      "metadata": {
        "year": 2004,
        "season": "Fall",
        "title_jp": "機動戦士ガンダムSEED DESTINY",
        "title_en": "Gundam SEED Destiny",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Gundam SEED Destiny\nJapanese title: 機動戦士ガンダムSEED DESTINY\nYear: 2004\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Gundam SEED Destiny — 2004 Fall."
      ]
    },
    {
      "id": "anime:2003-fall-fullmetal-alchemist-2003-4d3838868f",
      "source_type": "anime",
      "title": "Fullmetal Alchemist (2003)",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2003",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "fall"
      ],
      "description": "Fullmetal Alchemist (2003) — 2003 Fall",
      "metadata": {
        "year": 2003,
        "season": "Fall",
        "title_jp": "鋼の錬金術師",
        "title_en": "Fullmetal Alchemist (2003)",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Fullmetal Alchemist (2003)\nJapanese title: 鋼の錬金術師\nYear: 2003\nSeason: Fall\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Fullmetal Alchemist (2003) — 2003 Fall."
      ]
    },
    {
      "id": "anime:2003-summer-da-capo-9b9dfb9119",
      "source_type": "anime",
      "title": "Da Capo",
      "url": "https://gabrielkoo.com/anime/",
      "canonical_url": "https://gabrielkoo.com/anime/",
      "published_at": "2003",
      "last_verified_at": "2026-08-23",
      "tags": [
        "anime",
        "summer"
      ],
      "description": "Da Capo — 2003 Summer",
      "metadata": {
        "year": 2003,
        "season": "Summer",
        "title_jp": "D.C.～ダ・カーポ～",
        "title_en": "Da Capo",
        "starred": false,
        "seichi": false
      },
      "content": "English title: Da Capo\nJapanese title: D.C.～ダ・カーポ～\nYear: 2003\nSeason: Summer\nFavourite: no\nVisited a real-world location associated with this title: no",
      "excerpts": [
        "Da Capo — 2003 Summer."
      ]
    }
  ],
  "resources": {
    "gabrielkoo://profile/public": {
      "name": "Gabriel Koo public profile",
      "description": "Public biography and engineering context from gabrielkoo.com.",
      "url": "https://gabrielkoo.com/about/",
      "last_verified_at": "2026-08-23",
      "text": "I was the second engineer at a virtual insurer in Hong Kong. That number sounds more impressive than it was — it mostly meant I owned everything nobody else had time for. The first few years were the unglamorous half of a startup: CI/CD, zero-trust networking, device management, the on-call rotation, the SSO integration that breaks at 2am. None of it demos well. All of it is why the company still runs. Today I lead the security and IT function, and I spend most of my time on the internal AI platform — the MCP servers, connectors, and guardrails that let an agent reach our data warehouse, our ticketing system, and our internal APIs without also being able to drop a table or deploy to production. The framing I keep coming back to is not \"how do we adopt AI.\" It's: make the safe path the fastest path, then watch where people actually go. Usage tells you more than any policy document. ## A few things I believe more strongly than I did three years ago ### Domain experts build better internal tools than engineers do They know exactly which twenty seconds of the workflow hurt, because they live in it. Engineers guess. Give a non-engineer a platform, a guardrail, and permission to make something ugly, and they'll out-ship the backlog on the problems that actually annoy people. My favourite outcomes at work aren't things I built. They're the tools someone else built after I stopped explaining and started unblocking. ### The constraint is ownership, not speed Most things that stall in an organisation aren't technically hard. They're just unowned. Two teams each assume it's the other's, and it sits there for a quarter. So the default move is to pick it up — claim it, ship something, and hand it off once it has a shape someone can inherit. Waiting for a mandate is how work dies quietly. ### Guardrails beat gates Security that blocks work gets routed around, usually into a personal account where you can't see it at all. I'd rather build the paved road: the sanctioned thing that is also the convenient thing. In a regulated business this is the whole game — the answer is rarely \"no,\" it's \"yes, through here, and here's why the fence is where it is.\" ### Taste transfers; syntax doesn't Hard skills have never been cheaper. What's scarce is knowing which problem is worth solving, when a prototype is good enough, and when a shortcut is the kind you'll pay for later. So when I work with people, I try to think out loud — not to teach a tool, but to make my reasoning inspectable enough that they can disagree with it. ### Make the reasoning public Internal bots that answer in public channels teach the whole room, not one person. Same reason I'd rather be challenged in the open than agreed with privately. The audit trail is a side benefit; the real value is everyone getting to see how a decision got made. ## Still hands-on I still merge PRs. I don't trust an opinion I can't reproduce, and I've found that leaders who stop touching the system slowly start optimising for the version of it that lived in their head three years ago. Currently interested in: agent harnesses, why every company rebuilds the same Slack bot, and how long \"human in the loop\" survives contact with volume. Most of what I've built is invisible when it's working. That's the point. ## Before this I read Actuarial Science at HKU (BSc, 1st class honours) and did interest rate modelling for my final year project, then spent the early years at a small startup turning into a data scientist by necessity — scraping, dashboards in `d3.js`, weekly machine learning tournaments on [Numer.ai](https://numer.ai), and a rule-based horse racing model built off my own HKJC scraper that made just enough money to be suspicious of itself. The maths came first, the infrastructure came later, and the AI work is where both finally pay off. Four AWS certifications from that transition, all still on the wall: - Developer – Associate (Apr 2018) - SysOps Administrator – Associate (Apr 2018) - DevOps Engineer – Professional (Feb 2019) - Security – Specialty (May 2019) I also build in the open when I can — for example [bedrock-access-gateway-function-url](https://github.com/gabrielkoo/bedrock-access-gateway-function-url), an OpenAI-compatible gateway for Amazon Bedrock. For the job history proper, see my [LinkedIn](https://linkedin.com/in/gabrielkoo). I don't like to mix my personal stuff with companies' stuff. ### AWS Community Builder I've given [a few talks at AWS events](/events), and in 2022 I joined the [AWS Community Builders](https://aws.amazon.com/developer/community/community-builders/community-builders-directory/?cb-cards.q=Gabriel%2BKoo) programme. These days I contribute to the AWS community in Hong Kong and write on . --- ## Personal Based in Hong Kong. Cantonese and English, usually in the same sentence. Outside work I break my own hardware, run more compute at home than is defensible, and read release notes for fun. I know Japanese though I only took JLPT N4... After that I studied in Sophia University (Tokyo) for two months for Japanese Language & Culture courses. And then, I travelled around Japan's coasts for 5 weeks, with a pair of 21 days + 14 days JR Passes. Do approach me if you got your crazy plan on travelling in Japan with JR, I might help you! ### Seichimeguri (聖地巡礼) I have a habit of visiting real-world locations that appear in anime — what Japanese fans call seichimeguri (聖地巡礼). This has taken me to Fukui (Glasslip, 2014), Numazu (Love Live Sunshine), Uji (Hibike! Euphonium), Takayama (Hyouka), London (K-on! Movie), and Italy (Love Live Sunshine!! Over the Rainbow). ### Aqours (2016–2020) From 2016 to 2020, most of my non-work passion went into Aqours — the second-generation group from Love Live! Sunshine!!. I attended every major live concert, made multiple pilgrimages to their fictional hometown in Numazu, Shizuoka, and even flew to New York for an event. The chapter ended in February 2020 — CYaRon!'s first live in Fukuoka was my last in-person concert before COVID. The two quotes I live by both trace back to this era: 日々精進 (daily improvement) from voice actress Inami Anju, and 一期一会 (once-in-a-lifetime encounter) from Aida Rikako. They stuck. ### How I got here My security-first mindset traces back to 18 years as an altar server — liturgical precision means every step matters. My builder instinct comes from LEGO and origami since childhood. My actuarial training gave me the math. Somehow all of this converged into a DevSecOps engineer at an insurtech company. ### Favourite Quotes - 日々精進 - 一期一会 - Get the shit done. If you got OCD and want to do things better, do it on your own but please do not make others suffer with you just because of your OCD."
    }
  }
}
