A conversational AI shopping assistant for building custom PCs, powered by Amazon Bedrock AgentCore and the Strands SDK. Customers chat with an AI agent that helps them choose compatible parts within their budget, manages a shopping cart, and completes checkout with World ID proof-of-human verification.
┌──────────────────┐
│ Browser │
│ React + Vite │
└────────┬─────────┘
│ HTTPS
▼
┌──────────────────┐
│ CloudFront │
│ (CDN + Headers) │
└───┬──────────┬───┘
static │ │ POST /invocations
assets │ │
┌────────┘ ▼
▼ ┌──────────────────┐
┌──────────┐ │ API Gateway v2 │
│ S3 Bucket│ │ (HTTP, throttled) │
│ (frontend│ └────────┬───────────┘
│ build) │ │
└──────────┘ ▼
┌──────────────────┐
│ Lambda Proxy │
│ (Python inline) │
└────────┬─────────┘
│ invoke_agent_runtime
▼
┌──────────────────┐
│ Bedrock │ ┌─────────────────┐
│ AgentCore │──────▶│ Amazon Bedrock │
│ Runtime │ │ (Claude Sonnet) │
│ ┌────────────┐ │ └─────────────────┘
│ │ handler.py │ │
│ │ pc_agent.py│ │ ┌─────────────────┐
│ │ world_id.py│──┼──────▶│ World ID API │
│ └────────────┘ │ │ (Verify Proofs) │
└────────┬─────────┘ └─────────────────┘
│ │
│ ┌──────┴──────────┐
│ │ SSM Parameter │
│ │ Store │
└────────────────▶│ (RP signing key)│
│ └─────────────────┘
│
┌──────────────────┬┴───────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌────────────┐
│ Products │ │ Sessions │ │ Orders │
│ Table │ │ Table │ │ Table │
│ (DynamoDB) │ │ (DynamoDB) │ │ (DynamoDB)│
└────────────┘ └──────────────┘ └────────────┘
| Component | Technology | Purpose |
|---|---|---|
| Frontend | React 18 + Vite | Chat UI, product catalog, World ID v4 widget (@worldcoin/idkit v4) |
| CDN | CloudFront | Static hosting, security headers (CSP, HSTS) |
| API | API Gateway HTTP v2 | Single POST endpoint, rate-limited (10 req/s) |
| Proxy | Lambda (Python) | Translates browser requests to AgentCore invocations |
| Agent | Bedrock AgentCore + Strands SDK | Conversational PC building assistant with tools |
| Model | Claude Sonnet 4 (cross-region) | LLM for natural language understanding and tool use |
| Verification | World ID v4 | Orb-level proof-of-human verification at checkout (one purchase per person) |
| Storage | DynamoDB (3 tables) | Products, sessions, orders |
| IaC | AWS CDK (TypeScript) | Full infrastructure definition |
Browser API GW Lambda AgentCore DynamoDB Bedrock
│ │ │ │ │ │
│ POST /invocations │ │ │ │ │
│ {type:"message", │ │ │ │ │
│ content, session_id,│ │ │ │ │
│ session_token} │ │ │ │ │
│─────────────────────▶│────────▶│ │ │ │
│ │ │ invoke │ │ │
│ │ │──────────▶│ │ │
│ │ │ │ validate │ │
│ │ │ │ session token│ │
│ │ │ │─────────────▶│ │
│ │ │ │◀─────────────│ │
│ │ │ │ │ │
│ │ │ │ restore conversation │
│ │ │ │─────────────▶│ │
│ │ │ │◀─────────────│ │
│ │ │ │ │ │
│ │ │ │ Converse (tools + prompt) │
│ │ │ │──────────────────────── ▶│
│ │ │ │◀──────────────────────── │
│ │ │ │ │ │
│ │ │ │ [tool calls: browse, │
│ │ │ │ recommend, cart, etc.] │
│ │ │ │─────────────▶│ │
│ │ │ │◀─────────────│ │
│ │ │ │ │ │
│ │ │ │ save messages│ │
│ │ │ │─────────────▶│ │
│ │ │◀──────────│ │ │
│◀─────────────────────│◀────────│ │ │ │
│ {type:"agent_message", │ │ │ │
│ content, cart} │ │ │ │ │
Browser AgentCore World ID v4 API DynamoDB
│ │ │ │
│ "I'd like to checkout" │ │ │
│─────────────────────────▶│ │ │
│ │ checkout() tool │ │
│ │ → not verified │ │
│ │ → raise Interrupt│ │
│◀─────────────────────────│ │ │
│ {type:"interrupt", │ │ │
│ name:"proof_of_human"} │ │ │
│ │ │ │
│ POST {type:"rp_signature"} │ │
│─────────────────────────▶│ │ │
│◀─────────────────────────│ │ │
│ {rp_id, nonce, signature,│ │ │
│ created_at, expires_at} │ │ │
│ │ │ │
│ [User completes World ID │ │ │
│ orb verification via │ │ │
│ IDKitRequestWidget] │ │ │
│ │ │ │
│ POST world_id_proof │ │ │
│─────────────────────────▶│ │ │
│ │ POST /v4/verify/ │ │
│ │ {rp_id} │ │
│ │─────────────────▶│ │
│ │◀─────────────────│ │
│ │ (session_id, │ │
│ │ nullifier) │ │
│ │ │ │
│ │ check existing │ │
│ │ orders by │ │
│ │ nullifier_hash │ │
│ │─────────────────────────────────▶│
│ │◀─────────────────────────────────│
│ │ │ │
│ │ transact_write: │ │
│ │ create order + │ │
│ │ deduct balance │ │
│ │─────────────────────────────────▶│
│ │◀─────────────────────────────────│
│◀─────────────────────────│ │ │
│ {type:"order_confirmed", │ │ │
│ order_id, total, │ │ │
│ world_id_session} │ │ │
│ │ │ │
- AWS CLI configured with credentials for the target account
- Node.js >= 18
- Python >= 3.12
- Docker running (for building the AgentCore container image)
- AWS CDK CLI (
npm install -g aws-cdk) - Amazon Bedrock model access enabled for Claude Sonnet 4 in the target region
- A World ID app (see World ID Setup below)
World ID v4 provides proof-of-human verification using zero-knowledge proofs. This app uses it to enforce orb-level verification at checkout and limit each verified person to one purchase.
- Go to the World Developer Portal
- Sign in and create a new app
- Enable World ID 4.0 for the app
- Note the App ID (
app_...) and RP ID (rp_...) - Generate a signing key and store it in SSM Parameter Store:
aws ssm put-parameter --name /AnyCompanyAgent/WorldIDRPSigningKey --type SecureString --value '0x...'
- In the developer portal, create an action named
checkout(or your preferred name)
All World ID settings are centralized in the CDK stack (cdk/lib/anycompany-stack.ts). The frontend config is auto-generated from these values during deployment:
const WORLD_ID_APP_ID = 'app_your_app_id_here';
const WORLD_ID_ACTION = 'checkout';
const WORLD_ID_RP_ID = 'rp_your_rp_id_here';
const RP_SIGNING_KEY_SSM_PARAM = '/AnyCompanyAgent/WorldIDRPSigningKey';- User asks the agent to check out
- Agent's
checkout()tool raises anInterruptExceptionrequesting proof of human - Frontend requests an RP context signature from the backend (secp256k1 ECDSA + keccak256, matching the
@worldcoin/idkit-serversigning algorithm) - Frontend displays the World ID verification modal (via
@worldcoin/idkitv4IDKitRequestWidgetwithorbLegacy()preset) - User completes orb-level verification through the World ID bridge
- IDKit returns a zero-knowledge proof to the frontend
- Frontend sends the proof to the backend
- Backend verifies the proof against the World ID v4 API
- Backend validates the credential is orb-level (
issuer_schema_id: 1) - Backend checks no existing order exists for this
nullifier_hashvia thenullifier-hash-indexGSI (one purchase per person) - On success, the order is created via a DynamoDB transaction
One purchase per person: Enforced at three layers: (1) a fast-path GSI query on nullifier-hash-index provides early rejection before reaching the transaction, (2) an atomic purchase_lock_{world_id_session} item in the DynamoDB order transaction prevents concurrent races by the same World ID session, and (3) an atomic nullifier_lock_{nullifier_hash} item prevents the same verified person from ordering even across different sessions. Both lock items use attribute_not_exists conditions — if two concurrent requests race, only one succeeds.
# 1. Install CDK dependencies
cd cdk && npm install
# 2. Store your World ID RP signing key in SSM (one-time setup)
aws ssm put-parameter --name /AnyCompanyAgent/WorldIDRPSigningKey --type SecureString --value '0x...'
# 3. Deploy (builds frontend, generates config, deploys everything)
npx cdk deploy
# 4. Seed the product catalog
cd ../scripts && npm install && node seed-products.jsA single cdk deploy handles everything:
- Builds the frontend with Vite (via local bundling, Docker fallback)
- Auto-generates
config.jswith the API Gateway URL and World ID settings - Deploys the agent container, infrastructure, and frontend to S3/CloudFront
After deployment, CDK prints:
Outputs:
AnyCompanyAgentStack.CloudFrontUrl = https://dXXXXXXXXXXXX.cloudfront.net
AnyCompanyAgentStack.ApiUrl = https://XXXXXXXXXX.execute-api.us-west-2.amazonaws.com/
AnyCompanyAgentStack.AgentRuntimeId = AnyCompanyAgent-XXXXXXXXXXXX
Open the CloudFront URL to use the app.
anycompany-agent/
├── agent/ # AgentCore container
│ ├── handler.py # HTTP entrypoint (/invocations)
│ ├── pc_agent.py # Strands agent with tools
│ ├── world_id.py # World ID v4 RP signing + proof verification
│ ├── tools/ # Agent tools package
│ ├── Dockerfile # Container image (Python 3.12-slim)
│ └── requirements.txt # Python dependencies
├── frontend/ # React SPA
│ ├── src/
│ │ ├── App.jsx # Main layout
│ │ ├── App.css # Application styles
│ │ ├── main.jsx # React entry point
│ │ ├── data/
│ │ │ └── products.js # Client-side product catalog (display only)
│ │ └── components/
│ │ ├── ChatPanel.jsx # Chat UI + session management
│ │ ├── WorldIdVerify.jsx # IDKit v4 widget with RP context
│ │ ├── Header.jsx # Category filter + search
│ │ ├── ProductCard.jsx # Product display card
│ │ └── ProductGrid.jsx # Product catalog grid
│ ├── config.js.template # Template for runtime config (envsubst placeholders)
│ ├── public/
│ │ └── config.js # Runtime config (gitignored, auto-generated by CDK)
│ ├── Dockerfile # Nginx container for Docker-based hosting
│ ├── .dockerignore # Docker build context exclusions
│ ├── index.html # HTML entry point
│ └── vite.config.js # Vite configuration
├── cdk/ # AWS CDK infrastructure
│ ├── lib/
│ │ └── anycompany-stack.ts # Full stack definition
│ └── bin/
│ └── app.ts # CDK app entry point
└── scripts/
└── seed-products.js # Seed DynamoDB with 30 PC parts
This file is auto-generated by CDK during deployment using s3deploy.Source.data. It is gitignored and should not be edited manually. CDK injects the API Gateway URL and World ID settings from stack constants:
window.APP_CONFIG = {
API_URL: "https://XXXXXXXXXX.execute-api.us-west-2.amazonaws.com/",
WORLD_ID_APP_ID: "app_your_app_id_here",
WORLD_ID_ACTION: "checkout",
WORLD_ID_RP_ID: "rp_your_rp_id_here"
};For local development, create this file manually or copy from config.js.template and fill in values.
A config.js.template with ${PLACEHOLDER} variables is also provided for the Docker/nginx hosting path (used by the Dockerfile with envsubst).
The AgentCore container receives these via CDK:
| Variable | Description | Default |
|---|---|---|
PRODUCTS_TABLE |
DynamoDB products table name | AnyCompanyAgentProducts |
SESSIONS_TABLE |
DynamoDB sessions table name | AnyCompanyAgentSessionsV2 |
ORDERS_TABLE |
DynamoDB orders table name | AnyCompanyAgentOrders |
WORLD_ID_ACTION |
World ID action identifier | checkout |
WORLD_ID_RP_ID |
World ID Relying Party ID | — |
RP_SIGNING_KEY_SSM_PARAM |
SSM parameter name for RP signing key (fetched at runtime) | /AnyCompanyAgent/WorldIDRPSigningKey |
The Lambda proxy receives:
| Variable | Description |
|---|---|
AGENT_RUNTIME_ARN |
ARN of the AgentCore runtime |
AWS_REGION_NAME |
AWS region for Bedrock calls |
ALLOWED_ORIGIN |
CloudFront domain for CORS |
ProductsTable — Product catalog
| Attribute | Type | Key |
|---|---|---|
id |
String | Partition Key |
category |
String | GSI Partition Key (category-index) |
price |
Number | GSI Sort Key (category-index) |
name, brand, description, specs |
String/Map | — |
SessionsTableV2 — User sessions (TTL: 24 hours)
| Attribute | Type | Key |
|---|---|---|
session_id |
String | Partition Key |
session_token_hash |
String | SHA-256 of session token |
cart |
List | Current shopping cart |
messages |
List | Conversation history (max 20) |
account_balance |
Number | Simulated balance ($10,000) |
human_verified |
Boolean | World ID verification status |
world_id_session |
String | World ID v4 session identifier |
world_id_verified_at |
String | Timestamp of World ID verification |
ttl |
Number | DynamoDB TTL epoch timestamp |
OrdersTable — Completed orders (PITR enabled)
| Attribute | Type | Key |
|---|---|---|
order_id |
String | Partition Key |
items |
List | Ordered products |
total |
Number | Order total |
nullifier_hash |
String | GSI Partition Key (nullifier-hash-index) — World ID nullifier, used for fast-path one-purchase-per-person check |
world_id_session |
String | GSI Partition Key (world-id-session-index) — World ID v4 session identifier |
session_id |
String | Originating browser session |
The Strands SDK agent has four tools:
| Tool | Parameters | Description |
|---|---|---|
browse_products |
category?, search?, max_price? |
Query the product catalog. Uses DynamoDB GSI for category filtering. |
recommend_build |
budget, use_case |
Generate a complete PC build recommendation. Allocates budget by component category based on use case (gaming, productivity, content creation, general). |
manage_cart |
action, product_id? |
Add, remove, view, or clear cart items. Updates DynamoDB immediately. |
checkout |
— | Initiate purchase. Checks balance, raises interrupt for World ID verification if not yet verified. |
Run the frontend locally with Vite:
cd frontend
npm run dev # Starts on http://localhost:5173For local development, create frontend/public/config.js with your deployed API URL (see Frontend Config), or set the VITE_API_URL environment variable.
# Create a session
SESSION=$(curl -s -X POST "$API_URL/invocations" \
-H "Content-Type: application/json" \
-d '{"type":"create_session"}')
SESSION_ID=$(echo "$SESSION" | jq -r '.session_id')
SESSION_TOKEN=$(echo "$SESSION" | jq -r '.session_token')
# Send a chat message
curl -s -X POST "$API_URL/invocations" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"message\",
\"content\": \"recommend a gaming PC for \$1500\",
\"session_id\": \"$SESSION_ID\",
\"session_token\": \"$SESSION_TOKEN\"
}" | jq .- Sessions are created server-side; the client never generates session IDs
- Session tokens are generated with
secrets.token_hex(32)(64 hex characters) - Tokens are stored as SHA-256 hashes in DynamoDB (cannot be reversed)
- Token validation uses
secrets.compare_digest()for constant-time comparison (prevents timing attacks) - Sessions expire after 24 hours via DynamoDB TTL
- Chat messages are limited to 4,000 characters
- All error responses use generic messages (no internal details leaked to clients)
- Agent system prompt includes instructions to resist prompt injection
- CloudFront: CSP, HSTS (2 years, preload), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin
- S3: Block all public access, OAC-only access from CloudFront
- API Gateway: CORS restricted to CloudFront domain, rate-limited (10 req/s, 20 burst)
- DynamoDB: Orders table uses
RETAINremoval policy with point-in-time recovery - IAM: Least-privilege roles scoped per service (DynamoDB table-level, SSM parameter-scoped, CloudWatch Logs account-scoped, ECR repository-scoped)
- Secrets: RP signing key stored in SSM Parameter Store (SecureString), fetched at runtime — never in environment variables, CloudFormation, or source code
- Container: Runs as non-root user with healthcheck
- Orb-level enforcement: Frontend uses
orbLegacy()preset; backend validatesissuer_schema_idin the v4 response (defense-in-depth) - One purchase per person: Enforced at three layers — a fast-path
nullifier-hash-indexGSI query for early rejection, an atomicpurchase_lock_{world_id_session}item in the order transaction, and an atomicnullifier_lock_{nullifier_hash}item in the order transaction (both useattribute_not_existsconditions to prevent concurrent races) - RP context signing: The signing key is stored in SSM Parameter Store (SecureString) and fetched at runtime via IAM — it is never passed as an environment variable or exposed in CloudFormation templates; the backend generates time-limited RP context signatures (secp256k1 ECDSA, 5-minute TTL)
- RP signature endpoint: Requires valid session credentials (prevents unauthenticated abuse)
- Proof sanitization: Only whitelisted fields from the IDKit result are forwarded to the World ID API
- Proofs are verified server-side against the World ID v4 API (never trusted client-side)
- World ID session IDs and nullifiers are stored on orders for auditability
- Orders use DynamoDB transactions (
transact_write_items) for atomic balance deduction and order creation, preventing race conditions - The
has_existing_ordercheck fails closed — blocks purchases if the orders table is unreachable
- All exceptions are caught and logged with type information only
- Client-facing error messages are generic and never include stack traces or internal state
- World ID error codes are mapped to user-friendly messages