> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baselayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Where to Run the Check

> Choose where checks run so they fit your setup: your backend, your login server or the edge.

> [!NOTE]
>
> The KYA SDK is coming soon. The code on this page won't run until the SDK packages are published.

The SDK runs wherever your code runs. The TypeScript SDK works in Node 20+, Auth0 Actions, Cloudflare Workers and other edge runtimes. The Python SDK works in Python 3.11+.

The setup is the same in every pattern: install the SDK, publish your `/.well-known/kya-profile.json`, add the nonce and token routes, and answer protected pages with `401` and a `KYA-Disclosure-Scope` header (see [Accepting agents](https://docs.baselayer.com/docs/kya-accepting-agents.md#set-it-up)).

The agent also does the same thing in every pattern (see [Presenting a Credential](https://docs.baselayer.com/docs/presenting-and-verifying-a-credential.md#present-by-hand)):

1. It asks for a nonce with `POST /nonces`.
2. It trades its credential for your access token with `POST /oauth/token`.

The agent side of the SDK uses those two paths by default. The only thing that changes is which of your systems answers them.

| Run it in | Best when | What receives the result |
| - | - | - |
| [Your backend](#in-your-backend) | One app or one protected workflow is adopting KYA first | The application code making the decision |
| [Your login server](#in-your-login-server) | You already issue access tokens centrally | Your existing APIs, which receive your normal token |
| [The edge](#at-the-edge) | You need one check in front of several services | Your origin servers, which receive the result from the worker |

## In your backend

Start here for a first integration, or when only one part of your site (such as checkout) accepts agents.

Your app server answers both routes:

* On `/nonces` it calls `verifier.issueNonce()` and returns `{ "nonce": "...", "audience": "shop.example" }`.
* On `/oauth/token` it calls `verifier.verify(subjectToken, { scope })` with the scope from your published profile.

Your app code receives the result directly: `claims.sub`, the checks that passed (`claims.verification.active_keys`) and the shared fields. It applies your rules and returns an access token for the agent, or refuses with the failure code.

Every server that answers these routes needs the same nonce store. See [Share nonces across servers](#share-nonces-across-servers).

## In your login server

Use this if you already run an OAuth authorization server. Your APIs keep checking your own tokens and never need to know about KYA.

Your login server answers both routes. It returns a nonce and its audience on `/nonces`, and adds a token exchange grant ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693)) on `/oauth/token`.

That request is form-encoded with these fields:

| Field | Value |
| - | - |
| `grant_type` | `urn:ietf:params:oauth:grant-type:token-exchange` |
| `subject_token_type` | `urn:baselayer:oauth:token-type:kya-credential` |
| `subject_token` | The credential. |
| `scope` | Optional. See [Asking for fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md). |
| `actor_token` | For a business agent, the acting person's credential. |

The login server:

1. Calls `verifier.verify()` and maps the result into your token: `claims.toTokenClaims()` gives `sub` and `exp`.
2. Responds with `access_token`, `token_type` and `expires_in`.

To tie your token to the agent's key, use `claims.cnf.jwk` (the agent's public key) and return `token_type: "DPoP"`.

Your APIs receive the same kind of token your other clients send: `Authorization: DPoP <token>` with a DPoP proof, or `Bearer` if you issue plain tokens. They check it as they do today.

Set your token's expiry from `claims.exp` so it never outlives the credential. When it expires, the agent presents a fresh credential. There are no refresh tokens.

## At the edge

Use this when one check should sit in front of several services. A CDN worker in front of your site (for example, Cloudflare Workers) answers both routes and runs the check, so your servers never touch the credential.

The worker answers `/nonces` from a shared nonce store (see [Share nonces across servers](#share-nonces-across-servers)) and calls `verifier.verify()` on `/oauth/token`.

Your origin servers receive the result attached to each request, for example the customer ID and the checks that passed, in request headers you define. They read those headers and apply your rules.

> [!WARNING]
>
> Strip those headers from all incoming traffic at the worker. Otherwise an agent can set them itself and skip the check.

## Tips for production

### Start in watch-only mode

Check every credential and log the result, but don't block anyone yet. Once the logs look right, turn on your rules.

```ts TypeScript theme={null}
const report = await verifier.verify(subjectToken, { scope });
console.log(report.failure ?? "accepted", report.checks);
// Continue as you do today, whatever the result.
```

```python Python theme={null}
report = await verifier.verify(subject_token, scope=scope)
print(report.failure or "accepted", report.checks)
# Continue as you do today, whatever the result.
```

### Decide what happens when Baselayer can't be reached

The SDK needs Baselayer's signing keys and revocation list. Both are cached, but if a fetch fails the check returns `ISSUER_RESOLUTION_FAILED` or `STATUS_LIST_RESOLUTION_FAILED`. These are the only two retryable failures.

The SDK never accepts a credential it couldn't check. What you do with the request is up to you:

* Fail open: treat the agent as an unverified visitor and let it continue with whatever access an unverified visitor gets.
* Fail closed: return `503` so the agent retries later.

### Share nonces across servers

A nonce must work exactly once. If you run more than one server, serverless functions or many edge locations, a nonce issued on one could be spent again on another, and a copied presentation could be replayed.

`InMemoryNonceStore` keeps nonces in the memory of one process (they expire after 10 minutes), so it only protects a single long-lived process.

Every verifier needs a `NonceStore`, and there is no default. Give every instance the same store, and make sure that store can check and spend a nonce in one atomic step.

| Store | Works? |
| - | - |
| Redis (`GETDEL`) | Yes |
| Your SQL database (`UPDATE ... WHERE status = 'issued' ... RETURNING`) | Yes |
| Workers KV | No, because it can't check and spend a nonce in one step |

> **Implement a shared NonceStore**
>
> A `NonceStore` has a `ttlSeconds` property and three async methods: `issue`, `status` and `spend`.
>
> `spend` is what stops replays. It must check and spend the nonce in one atomic step and return the status from before the change.
>
> ```ts TypeScript theme={null}
> // The shape your store implements (from the SDK).
> interface NonceStore {
>   readonly ttlSeconds: number;
>   issue(): Promise<string>; // save a new random nonce as issued, return it
>   status(nonce: string): Promise<NonceStatus>; // read only: Issued, Spent or Unknown
>   spend(nonce: string): Promise<NonceStatus>; // atomic: mark spent, return the status from before
> }
> ```
>
> ```python Python theme={null}
> # The shape your store implements (from the SDK).
> class NonceStore(typing.Protocol):
>     @property
>     def ttl_seconds(self) -> float: ...
>
>     async def issue(self) -> str: ...  # save a new random nonce as issued, return it
>
>     async def status(self, nonce: str) -> NonceStatus: ...  # read only: Issued, Spent or Unknown
>
>     # Atomic: mark spent, return the status from before.
>     async def spend(self, nonce: str) -> NonceStatus: ...
> ```
>
> Then pass your store in when you create the verifier:
>
> ```ts TypeScript theme={null}
> import { KyaVerifier } from "@baselayer/kya";
>
> const verifier = new KyaVerifier({ audience: "shop.example", nonceStore: sharedNonces });
> ```
>
> ```python Python theme={null}
> from baselayer_kya import KyaVerifier
>
> verifier = KyaVerifier(audience="shop.example", nonce_store=shared_nonces)
> ```

### Share one verifier per process

The verifier holds the key and revocation list caches. Create it once and reuse it, rather than creating one per request.

### Returning customers across servers

The verifier keeps no record of which customers it has seen. To know a returning customer, look up `claims.sub` in your own database. See [Matching customers](https://docs.baselayer.com/docs/kya-matching-customers.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.