---
title: "Stamp Identity at the Door"
description: "Replace the throwaway header door from 2.3 with a real ordered auth walk that derives the caller's identity, and tier, from a session, then feeds the per-tier playbook."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/stamp-identity"
md_url: "https://vercel.com/academy/building-agents-with-eve/stamp-identity.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-08-08T22:50:09.159Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Stamp Identity at the Door

# Stamp Identity at the Door

In 2.3, we trusted an `x-shop-tier` header to test the per-tier playbook. Anyone who sent `x-shop-tier: pro` received the pro discount. Now that the dispatcher has web and Slack entrypoints, we'll derive the tier from an authenticated customer instead.

The job of a door is to answer one question before any work happens: *who is this?* In eve that's **route auth**, a policy on the channel that runs before the model does a thing. It decides who's allowed in, and it produces the caller's identity, the same `ctx.session.auth` the playbook reads. Auth helps us know our customer.

## Outcome

The dispatcher derives the caller's identity from a real session, stamps their `tier` from the shop's records, and feeds it to the per-tier playbook, with no way for a caller to set their own tier.

## Hands-on exercise

The `auth` option on the channel takes a list of functions eve walks **in order**. Each one does one of three things:

- returns a `SessionAuthContext` → accept the caller, stop the walk
- returns `null` → "not my caller," fall through to the next entry
- throws → reject with a specific status

If every entry falls through, the request gets a `401`. That ordered walk is the whole model: put your real auth first, then the framework's dev and OIDC helpers as catch-alls.

You need something that turns a request into a known customer. In a real app that's your auth provider (Clerk, Auth.js, your own OIDC). For the course, drop in a small stand-in:

```bash
curl -f --create-dirs -o agent/lib/auth.ts https://raw.githubusercontent.com/vercel-labs/bike-shop-agent/main/agent/lib/auth.ts
```

It exposes one function, `getCustomer(request)`, that reads a session cookie and returns the customer on file, *including their tier*:

```ts title="agent/lib/auth.ts (excerpt)"
export interface Customer {
  readonly id: string;
  readonly tier?: "member" | "pro";
}

// Reads the `shop_session` cookie and looks the customer up server-side.
export function getCustomer(request: Request): Customer | null;
```

The tier comes from the customer record looked up on the server, rather than a value supplied by the request. This closes the hole from 2.3.

Rewrite `agent/channels/eve.ts` so `appAuth` uses it:

- Call `getCustomer(request)`. No customer? Return `null` and fall through.
- Otherwise return a `SessionAuthContext`: the customer's `id` as `principalId`, `principalType: "user"`, an `authenticator` and `issuer` label, and `attributes.tier` *only when the record has one*.

\*\*Note: Identity is set once, and flows everywhere\*\*

This file stamps the caller's identity on `ctx.session.auth.current` for the session. The playbook from 2.3 reads `attributes.tier`, and a tool could use `principalId` to scope data. The runtime carries identity without passing it through each function.

## Try It

Restart the server. Testing a tier means calling as a specific customer, which means sending the `shop_session` cookie, so we use `curl` here rather than the dev TUI (the TUI always runs as a local dev caller with no cookie, the plain desk).

One thing about the API first: a `POST` to `/eve/v1/session` *starts* the turn and returns a session handle (`{ ok, sessionId, continuationToken }`), not the reply. The dispatcher's answer streams back from `/eve/v1/session/<id>/stream`. So each test is two calls, POST to start, then attach to the stream to read the answer. This shell helper does both, and filters the stream down to the words that reveal which playbook loaded:

```bash
ask() { # usage: ask <cookie> "<message>"
  sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
    -H 'content-type: application/json' \
    -H "cookie: shop_session=$1" \
    -d "{\"message\":\"$2\"}" \
    | sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
  curl -sN -H "cookie: shop_session=$1" \
    "http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
    | grep -iE "torque|overhaul|discount|loaner|tune-up"
}
```

Call as a pro:

```bash
ask demo-pro "rear shifting is sloppy, what do you suggest?"
```

The reply talks **torque** specs and recommends the **overhaul**, the pro playbook, because `getCustomer` returned `{ id: "cust_003", tier: "pro" }` and `appAuth` stamped `tier: "pro"`. Swap the cookie and the desk changes:

- `ask demo-member "rear shifting is sloppy, what do you suggest?"` → the 10% labor **discount** and a free **loaner**.
- `ask demo-walk-in "rear shifting is sloppy, what do you suggest?"` → a basic **tune-up** with no tier perks.

(Drop the `| grep …` line to read the whole reply; the filter is only there to make the difference jump out of the stream.)

Now try to cheat the 2.3 way, send the tier as a header with no session cookie:

```bash
sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -H 'x-shop-tier: pro' \
  -d '{"message":"rear shifting is sloppy, what do you suggest?"}' \
  | sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
curl -sN "http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
  | grep -iE "torque|overhaul|discount|loaner|tune-up"
```

You get the **plain desk**, the same `tune-up` as `demo-walk-in`. The header is ignored: with no `shop_session` cookie, `appAuth` finds no customer and falls through, so the tier never gets stamped. A walk-in can't talk, or header, their way into the pro discount anymore.

\*\*Note: What localDev and vercelOidc are doing in the list\*\*

`appAuth` handles your real customers. `localDev()` keeps loopback requests working while you develop (so `npx eve dev` and the local dashboard still talk to the agent), and `vercelOidc()` lets internal Vercel callers in. Order matters: real auth first, catch-alls last. Anything none of them recognizes gets a `401`, auth fails closed.

Getting a `401` for a customer that should work? Confirm `getCustomer` is reading the `shop_session` cookie and that `appAuth` is *first* in the `auth` array. If the tier doesn't take effect, check that you stamp `attributes.tier` from `customer.tier`. The playbook reads that exact path.

## Done-When

- [ ] `agent/channels/eve.ts` runs an ordered walk `[appAuth, localDev(), vercelOidc()]`.
- [ ] `appAuth` stamps `tier` from the customer record, not from a request header.
- [ ] A `shop_session=demo-pro` cookie produces the pro playbook; `x-shop-tier: pro` does nothing.
- [ ] An unrecognized caller falls through to the catch-alls (or `401` in production).

## Solution

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";
import { getCustomer } from "../lib/auth.js";

const appAuth: AuthFn<Request> = async (request) => {
  const customer = getCustomer(request);
  if (!customer) return null; // not one of our customers → fall through

  // The tier comes from the customer's record, not from the request. The
  // per-tier playbook (agent/skills/shop-playbook.ts) reads it from here.
  const attributes: Record<string, string> = {};
  if (customer.tier) attributes.tier = customer.tier;

  return {
    principalId: customer.id,
    principalType: "user",
    authenticator: "app",
    issuer: "spoke-and-mirror",
    attributes,
  };
};

export default eveChannel({
  auth: [appAuth, localDev(), vercelOidc()],
});
```

The per-tier playbook now runs from authenticated customer data instead of a test header. Section 5 confirms that production rejects callers the auth policy does not recognize.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
