DEV Community

Diana Aliya
Diana Aliya

Posted on

Build, Ship, Shape: Amazon Developer Hackathon

Alguard: a two-tier trust layer for agentic purchases

I just submitted Alguard to the
Amazon Developer Hackathon (Alexa+ track). This is a short write-up of the
architecture and the reasoning behind it.

The problem

An agent that can spend money is an agent that can be prompt-injected.
Classic defenses (LLM guardrails, prompting) are advisory — a
sufficiently clever injection gets around them. If Alexa+ can "just buy
it," the failure mode is real money lost.

The invariant

The system that requests a risky action is never the system that
approves it.

This is a structural rule, not a prompt. It shapes the whole server.

Two tiers

Tier 1 — the MCP server (mcp_server/server.py). Exposes exactly
three tools over Streamable HTTP (MCP spec 2025-11-25):

  • attempt_purchase
  • get_session_status
  • get_audit_trail

No approve tool. Not hidden, not guarded — absent from the schema.

Tier 2 — the human channel (web_api/resolve_action.py). A separate
FastAPI service on port 8010, behind its own OAuth audience and its own
approve scope. The only place a HALTED action can be resolved.

There is no network path from Tier 1 to Tier 2. The customer carries an
action_id by hand.

The risk engine

Every attempt is scored on:

  • Category severity — gift cards, crypto, wire transfers are High; groceries are Low
  • Anomalies — new merchant, amount spike vs. history, frequency spike, retry-after-halt
  • Owner trust — bumped +0.03 on approval, -0.15 on denial

Plus hard rules that no amount of trust bypasses: >$1000 single purchase,

$1500/h, >$3000/24h, High-severity category, retry-after-halt within 24h,
$250 at an unapproved merchant.

What made this hard

Concurrency. Two concurrent approvals must yield exactly one winner.
The resolver is a single conditional UPDATE with a rowcount == 1 check.

Crash consistency. If the process dies between "approved" and
"fulfilled," reconcile_unfulfilled() heals it on startup — idempotently.

Cross-process dedup. The in-process lock is a fast path, not the
guarantee. insert_action_if_not_duplicate() runs check + insert inside
BEGIN IMMEDIATE.

Bedrock is non-critical. The explanation text is generated by
converse, but a Bedrock failure never changes the decision — the
fallback template is honest about what it is (source = local-fallback).

Red-team

demo/showcase.py runs 15 attacks a hostile agent would try — category
lying, laundering a halt by retrying, session_id rotation, NaN amounts,
serial overspend. All caught. ALL ATTACKS HANDLED.

What I'd build next

  • Wire resolve_pending_action to a real charge (Checkout / ChargePermissionId)
  • Replace the text-heuristic category inference with MCC data
  • Split the shared SQLite into per-tier stores
  • Add EWMA-based drift detection (the current check catches spikes, not slow upward drift)

Repo:

Top comments (0)