
This article is part of the Building an E-commerce AI Agent with AWS Serverless series:
- Understanding AI Agents
- Choosing the Right Runtime
- Building Product Search and RAG
- Conversation Memory and Context Management
- Connecting the Agent to Your Webshop
- Security, Privacy, and Observability (here)
- The Sample Application
Source code: https://github.com/ServerlessLife/ecommerce-ai-agent
Authentication, Safety and Abuse Control
Our AI assistant is publicly accessible, so anyone visiting the webshop can start a conversation. This is important for the shopping experience, but it also introduces security and cost risks. Every message can trigger multiple LLM calls, product searches, and other operations that cost money.
Securing an AI agent is not just about authenticating users. We also need to control what the agent can access, which actions it can perform, how much it can cost, and how we protect it against misuse.
There are several ways to address these challenges. The right architecture depends on the webshop, its existing authentication system, and how much responsibility we want to give the agent.
Our solution is designed to integrate with existing webshops with minimal changes. We give visitors a separate identity for interacting with the agent, reuse the webshop's existing authentication for customer-specific operations, and execute selected tools in the browser.
Authentication
Our shopping assistant supports both anonymous visitors and registered customers. However, anonymous access does not mean that authentication is unnecessary.
We don't need to know who a visitor is in the real world, but we do need a way to recognize the same visitor across requests, manage their conversation, enforce usage limits, and block abuse. The simplest approach is to give every visitor a temporary identity, independent of the webshop's user accounts.
When the visitor opens the assistant, the browser requests a temporary identity and session from our backend. The backend issues a short-lived authentication token, which the browser includes in subsequent requests. Lambda validates the token before invoking the agent. A signed JWT is one option; an opaque, server-side session token can work just as well. The backend must assign and validate the identity instead of trusting a visitor ID supplied by the browser.
Keeping webshop authentication separate
Our shopping assistant uses two independent authentication mechanisms. The agent identifies visitors using temporary identities, while the webshop continues to manage its own customer authentication.
This means that every visitor can use the assistant, whether or not they are logged into the webshop. When a visitor signs in, the agent does not need to know their webshop credentials or receive their access token.
But what happens when the agent needs to perform an operation that requires an authenticated customer, such as checking an order or adding a product to the cart?
Our approach is to execute these tools in the browser instead of on the agent server. This client-side tool execution flow is explained in Part 1's Tools can be executed on a client.
When the LLM requests such a tool, the agent forwards the request to the browser using AG-UI. The browser executes the action through the webshop's existing functionality and returns the result to the agent. The webshop backend handles authentication and authorization exactly as it would for a normal customer action.

This has two important advantages:
- Simpler integration: We can reuse existing webshop functionality, such as adding products to the cart and retrieving orders, without implementing the same operations again in the agent backend.
- Simpler security: The AI agent does not need access to customer authentication tokens or direct permissions to customer accounts. Authentication and authorization remain the responsibility of the webshop.
For example, when a customer asks the agent to add a product to the cart, the browser can call the same cart functionality used by the webshop's "Add to cart" button. The agent receives the result and can confirm whether the operation succeeded. Part 5's Connecting the agent to the shop chapter describes the tools used in our shopping assistant, including the browser-executed cart and order-history tools.
This architecture works particularly well for our project because we want to add an AI assistant to existing webshops without redesigning their authentication systems.
It is not the only possible solution. An agent could also execute authenticated operations on its server, using delegated customer authorization and appropriately scoped credentials. That approach may be preferable for some applications, but it requires the agent backend to take on additional responsibilities for credential handling and access control.
JWT authentication with AgentCore Runtime
Although we've chosen AWS Lambda for our shopping assistant, it's worth considering how authentication would work with Amazon Bedrock AgentCore Runtime.
AgentCore Runtime supports IAM (SigV4) and JWT bearer-token authentication for incoming requests. IAM is a natural fit for AWS service-to-service calls; JWT authentication is convenient for a browser-based assistant.
AgentCore can validate JWTs issued by a compatible OAuth 2.0/OpenID Connect (OIDC) identity provider before requests reach the agent. We could use Amazon Cognito, but creating and managing Cognito users for every anonymous visitor would be unnecessary for our use case.
Instead, we could build a lightweight OIDC-compatible token issuer, with signing keys published through a JWKS endpoint and suitable discovery metadata. It would assign temporary visitor identities and issue short-lived JWTs with the issuer, audience, and claims expected by AgentCore.
You can find an implementation of this lightweight token issuer in our sample project. The Lambda function exposes the OIDC discovery and JWKS endpoints used by AgentCore to validate JWTs, as well as an endpoint for issuing tokens. You can learn more about the sample project in Part 7: The Sample Application.
The flow is straightforward:
The browser requests a token from our identity service.
The service checks whether the visitor has exceeded their limits or been blocked.
If permitted, it returns a signed JWT.
The browser sends the JWT when invoking AgentCore Runtime.
AgentCore validates it before forwarding the request to our agent.
A valid JWT does not guarantee that a visitor remains within their usage limits. AgentCore's token validation does not enforce our application-specific quotas, so the application (or another enforcement layer) must still check them. A short token lifetime enables rechecking at renewal, but an issued token normally remains valid until expiration unless another layer blocks the request.
Do you need temporary or permanent help on your project?Rate Limiting and Cost Control
Authentication allows us to identify visitors and block abusive users, but authentication alone does not control how much they can use the agent or how much it costs us.
Unlike a traditional webshop API, where a request usually performs a predictable operation, an AI agent can make multiple LLM calls, search for products, invoke tools, and repeat these steps before producing an answer. This makes the cost of individual requests difficult to predict.
We need to protect our application at two levels. First, we must limit how much work the agent can perform while answering a single question. Second, we must limit how frequently visitors can use the agent and how much they can consume over time.
Limiting agent execution costs
A single user message can trigger multiple LLM calls and tool executions. If the agent gets stuck in a loop, repeatedly searches for products, or processes an excessively long conversation, even one request can become expensive.
Fortunately, we can control much of this behavior at the application level. For example, we can configure the maximum number of agent-loop iterations, restrict tool calls, enforce execution timeouts, and limit the size of requests and conversation history.
These controls prevent an individual question from consuming excessive resources, but they do not stop a visitor from sending hundreds of otherwise valid questions. For that, we need usage limits across requests.
Rate limiting with DynamoDB
DynamoDB provides a simple, scalable serverless way to enforce usage limits across multiple agent requests.
We can maintain counters per temporary visitor identity, IP address, and time window, with additional shop-wide limits when necessary. For example, we might allow 20 messages per visitor in a 10-minute window.
| PK | Count |
|---|---|
| USER123#12:10-12:20 | 2 |
| USER456#12:10-12:20 | 8 |
Before invoking the agent, Lambda increments the relevant counter only if the current count is below the configured maximum:
await db.send(
new UpdateCommand({
TableName: "ChatLimits",
Key: { pk: windowKey },
UpdateExpression: "SET expiresAt = :ttl ADD #n :one",
ConditionExpression: "attribute_not_exists(#n) OR #n < :max",
ExpressionAttributeNames: { "#n": "count" },
ExpressionAttributeValues: {
":ttl": expiryUnixSeconds,
":one": 1,
":max": 20,
},
}),
);
The condition and increment are atomic, even when multiple requests arrive simultaneously. When the maximum is reached, DynamoDB returns ConditionalCheckFailedException, and Lambda rejects the request before invoking the agent.
DynamoDB TTL can clean up expired counters automatically.
Counting requests is only the simplest example. We can use the same approach to track token consumption or estimated LLM costs per visitor, session, or webshop.
We should also combine limits per visitor with limits per IP address and potentially shop-wide limits. Visitor identities can be recreated, while IP addresses can be shared or changed, so neither is sufficient on its own.
Combining execution limits with DynamoDB usage counters gives us control over both individual expensive requests and excessive usage over time. It also gives us a foundation for identifying abusive behavior and temporarily blocking visitors before they generate additional costs.
Safety, Guardrails and Abuse Prevention
A public shopping assistant can say the wrong thing, take the wrong action, or incur costs without creating value. We need protections in the application as well as in the prompt.
What can go wrong?
Some visitors will deliberately test the assistant's limits. They might try to override its instructions, extract its system prompt, or steer it to unrelated tasks such as writing code instead of discussing products.
There is also a cost risk: automated scripts can send many long questions, create sessions repeatedly, or intentionally sustain expensive conversations.
Several public incidents show how unexpected AI behavior can become a reputational problem:
- DPD chatbot — a customer persuaded DPD's support chatbot to swear, call itself useless, and criticize DPD

- Chevrolet dealership chatbot — a user manipulated a dealership chatbot into apparently agreeing to sell a Chevrolet Tahoe for $1. No vehicle was actually sold, but the conversation went viral

Good instructions, guardrails, rate limits, and application controls reduce these risks, but cannot eliminate them.
Instructions, guardrails and blocking abusive visitors
Modern LLMs are generally good at following instructions about what they should and shouldn't answer. However, they are not foolproof. Users can still bypass these restrictions through prompt injection or jailbreak attempts. Using an additional LLM call to validate every request would also be expensive, especially for a public shopping assistant handling many conversations.
This is where Amazon Bedrock Guardrails becomes useful. It provides a dedicated, more cost-effective way to validate requests before sending them to the LLM. Guardrails can also evaluate model responses for harmful content, denied topics, prompt attacks, and sensitive information. When a prompt is blocked before inference, we avoid paying for the main model invocation. Guardrail evaluation has its own charge, so actual savings depend on the configured policies and traffic.
There are two integration options:
- When using Amazon Bedrock models: Guardrails can be integrated directly into model calls. Bedrock automatically checks the user's input and the model's response, with minimal additional implementation.
- When using models from other providers: We can still use Bedrock Guardrails, but we need to integrate them ourselves. Our application must explicitly send the user's input to Guardrails before calling the LLM and optionally check the generated response afterward.
Bedrock Guardrails offers two safeguard tiers: Classic and Standard. Standard provides stronger protection against prompt attacks and prompt leakage, along with much broader language support. However, language support varies by feature. Standard also uses cross-Region inference, which may affect data residency requirements.
We should record repeated attempts at prompt injection, extraction, or other misuse, and apply temporary blocks or stricter limits to the agent identity and, when appropriate, the IP address. Blocked visitors can then be rejected before another expensive model call. Guardrails supplement—not replace—tool authorization, application checks, and usage limits.
Customer privacy and LLM providers
A webshop AI assistant may process personal information such as names, contact details, order information, or account data. Users may also provide sensitive information even when we do not explicitly ask for it:
“I have diabetes. Which product is suitable?”
“Can I use this while pregnant?”
“I take medication for high blood pressure.”
Health information deserves particular care. Under GDPR it is a special category of personal data. In the United States there is no single equivalent to GDPR: privacy requirements come from federal rules, state privacy laws, and sector-specific regulation. HIPAA, for example, normally applies to healthcare organizations and their business associates rather than an ordinary webshop, but consumer health information can still be protected by FTC rules and state laws.
Our architecture should therefore assume that personal and sensitive information can appear in any conversation, regardless of whether the assistant was designed to collect it.
What matters when choosing an LLM provider
Several separate questions matter when customer conversations are sent to an LLM:
- Data residency — where the request is decrypted and processed.
- Retention — whether the provider stores prompts and responses, and for how long.
- Training and data collection — whether customer data can be used to improve models or for other purposes.
- Subprocessors — which additional companies may process the data.
- Contractual protection — what obligations the provider accepts when processing personal data. For GDPR, this normally includes a Data Processing Agreement (DPA), also called a Data Processing Addendum. It defines the obligations of a processor handling personal data on our behalf.
A DPA is separate from data residency: having a DPA does not mean that processing stays inside the EU.
Similarly, Zero Data Retention (ZDR) is not the same as regional processing. ZDR controls whether prompts and responses are retained after inference; regional routing controls where inference and related processing take place.
Provider options
Amazon Bedrock is a natural option when the rest of the application already runs on AWS. AWS states that customer prompts and outputs are not shared with the underlying model providers and are not used to train the models. Bedrock also gives us control over the AWS Region in which inference runs, although individual models can have additional retention rules that should be checked before use.
Mistral is an interesting direct European provider. Its data is hosted in the EU by default, although some features can involve subprocessors outside the EU. It also offers ZDR for supported stateless API calls.
OpenAI, Azure OpenAI, and Google Vertex AI can also be appropriate when their regional-processing, retention, and contractual options match our requirements. These capabilities should be checked for the exact API and model we plan to use rather than assumed from the provider name alone.
For Anthropic Claude, we can either use Anthropic directly or access Claude through a platform such as Bedrock or OpenRouter. Those options can have different data-processing and retention characteristics even though the underlying model is the same.
Do you need temporary or permanent help on your project?OpenRouter: regional routing and privacy controls
OpenRouter is useful when we want access to many model providers through one API while keeping the ability to change models easily.
Some of the most interesting LLMs today are inexpensive, high-performing open-weight models from Chinese companies. Because their weights are publicly available, they can be hosted by providers anywhere in the world, not necessarily in China.
We would probably use OpenRouter to access these models. It offers a single API with multiple hosting providers, making it easy to compare models and switch between them.
However, where a model was developed and where our customer data is processed are two different things. For privacy and GDPR compliance, what matters is the actual inference provider, processing location, retention policy, and contractual protections.
Its In-Region Routing supports both EU and US processing. Requests sent through the regional endpoint are decrypted inside the selected region and routed only to provider endpoints operating there. If no suitable in-region provider can serve the selected model, the request fails rather than falling back to another region.
Simply selecting a model from a US or European company does not guarantee that the actual inference happens there. Regional routing controls the location of the processing path, not the nationality of the company that created the model.
OpenRouter provides several independent privacy controls:
- In-Region Routing controls where the request is processed.
zdr: true restricts routing to providers that offer Zero Data Retention.data_collection: "deny"restricts routing to providers that do not collect the request data for non-transient purposes such as training.
These controls can be combined. For example, an EU-routed request with ZDR and data collection disabled can require that processing remains in the EU, that prompts and responses are not retained by the inference provider, and that they are not collected for training. If no provider satisfies the restrictions, the request fails.
OpenRouter describes this combination as part of its Sovereign AI approach: keeping workloads within a chosen jurisdiction while separately controlling retention and data collection.
Recommended approach
For this webshop assistant, a good baseline is:
appropriate regional processing + minimal retention or ZDR + no training on customer conversations + minimal application logging + appropriate contractual protection.
The most interesting options are:
- Amazon Bedrock — a straightforward choice when we want to keep the architecture and provider relationship inside AWS.
- OpenRouter with In-Region Routing + ZDR — attractive when model flexibility is important while still enforcing regional processing and retention requirements.
OpenRouter is particularly attractive when we want to experiment with or switch between models. Bedrock is attractive when reducing the number of external processors and keeping the architecture within AWS is more important.
Observability
AI agents are non-deterministic. The same question can lead to different decisions, tool calls, and responses. Unlike a traditional application with a predictable execution path, an agent decides what to do while it is running.
This makes debugging more difficult. When a customer receives a slow or incorrect answer, we want to know what happened. Which tools did the agent call? What parameters did it pass? What did those tools return? How often did it call the LLM? Where was the time spent?
Application logs help, but they rarely provide the complete picture. Distributed tracing is the best way to understand an agent's execution across model calls, tools, databases, and external services.
The good news is that much of the instrumentation is already available. Strands Agents supports OpenTelemetry, AWS offers ADOT instrumentation, and CloudWatch can collect and visualize traces. We don't need to build a tracing system ourselves.
Distributed tracing with OpenTelemetry
OpenTelemetry (OTel) is an open standard for collecting traces, metrics, and logs. A trace follows related operations across an agent's execution. Individual spans record operations and their duration, with optional attributes and error details.
For our shopping assistant, traces can reveal model calls and token usage, tool names and execution times, tool inputs and outputs where content capture is enabled, and calls to services such as Bedrock and DynamoDB. Exactly which fields appear depends on the framework, instrumentation, sampling, and configuration.
If an answer takes eight seconds, tracing can show whether the delay came from model inference, product search, reranking, or another operation. We can also inspect the sequence of calls to understand the agent's behavior.
Strands Agents can emit agent-specific spans. AWS Distro for OpenTelemetry (ADOT) can automatically instrument supported Node.js libraries, including HTTP clients and the AWS SDK. We can add custom spans where the automatically captured data is insufficient.
AgentCore and Lambda: Different Observability Integrations
Both AgentCore Runtime and Lambda support full distributed tracing of our AI agent. Since we use Strands Agents, which has built-in OpenTelemetry instrumentation, we can capture the agent's execution, including LLM calls, tool executions, token usage, and execution times, regardless of where the agent runs.
AgentCore Runtime integrates natively with CloudWatch GenAI Observability. AWS provides the runtime integration and exports the agent's OpenTelemetry traces to CloudWatch. With observability enabled and Strands properly instrumented, we can explore conversations, agent sessions, model calls, and tool executions through the GenAI Observability dashboard.
Lambda has native integration with AWS X-Ray, which provides excellent distributed tracing for traditional serverless applications. With active tracing enabled, X-Ray helps us understand how requests move through Lambda and other AWS services, identify performance bottlenecks, and investigate failures. Combined with the OpenTelemetry traces generated by Strands Agents, we can also examine the internal execution of our AI agent.
We can also use CloudWatch GenAI Observability with agents running on Lambda. GenAI Observability is not limited to AgentCore Runtime. We can configure AWS Distro for OpenTelemetry (ADOT) to export Strands telemetry from Lambda to CloudWatch, where it can be displayed in the GenAI Observability dashboard.
GenAI Observability
CloudWatch GenAI Observability provides an agent-oriented view of traces and metrics. Instead of examining unrelated log messages, we can inspect an agent's sessions, execution steps, model usage, and errors in one place.
The following screenshots are from our AgentCore Runtime deployment. They illustrate AgentCore observability.



These views help us follow the sequence of model and tool operations, investigate slow answers, and identify failures. The available detail depends on which spans and attributes our application emits.
Setting up distributed tracing
We configure the tracing pipeline and then enable instrumentation for the runtime. CloudWatch Transaction Search must be enabled in the AWS account and Region to use the AgentCore trace views shown above. It stores ingested spans in CloudWatch Logs and offers trace search and visualization. Span capture and the separate percentage indexed for X-Ray summaries affect visibility and cost.
CloudWatch Transaction Search: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Enable-TransactionSearch.html
For AgentCore Runtime, enable observability for the runtime and instrument the Node.js agent with ADOT.
AgentCore Observability setup: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability-get-started.html
Node.js direct-code deployment: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-get-started-code-deploy-node.html#step-4-enable-observability-node
For traditional Lambda Event Functions, AWS provides a managed ADOT JavaScript layer. Configure the layer, tracing permissions, and exporter according to the Lambda and ADOT documentation. Basic X-Ray tracing does not require CloudWatch Transaction Search, but the CloudWatch span-based GenAI workflow described here does. Lambda Web Functions use a different runtime model, so do not assume that traditional Lambda layer instructions apply unchanged.
Lambda Node.js tracing: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-tracing.html
Exporting traces from Lambda
Lambda can freeze its execution environment as soon as a request completes. If OpenTelemetry still has buffered spans, they may not be exported. The AWS-managed ADOT Lambda Layer handles trace exporting, so no additional application code is normally needed. With a custom OpenTelemetry setup, call forceFlush() before returning from the handler to ensure pending spans are exported.
Node.js auto-instrumentation and CommonJS
We can create spans manually or use auto-instrumentation, which instruments supported libraries as they load. For this project, auto-instrumentation reduces the amount of tracing code we need to maintain.
The AWS-documented AgentCore Node.js ADOT auto-instrumentation path requires CommonJS (CJS) output because it patches require() calls. We can continue writing TypeScript with import and export syntax, but compile the deployed code to CommonJS for that setup. OpenTelemetry also supports ESM through other instrumentation approaches; this is not a general limitation of OpenTelemetry or Node.js.
Auto-instrumentation consumes resources. Enabling ADOT auto-instrumentation can increase memory usage by approximately 10–30%. Memory consumption directly affects AgentCore Runtime and Lambda Web Function billing; traditional Lambda Event Functions instead bill according to configured memory and execution duration.
