.NET
AI agent detection for ASP.NET Core and .NET Framework
Overview
Vouched KYA for .NET works on ASP.NET Core (.NET 8, 9 and 10) and .NET Framework (4.6.2 through 4.8.1). One package, pick your runtime, ship.
Prerequisites
- A Vouched KYA project: get your Project ID and API key from Installations in the dashboard.
- An ASP.NET Core (.NET 8, 9 or 10) or .NET Framework (4.6.2โ4.8.1) application. .NET Framework requires a 64-bit host process: see Troubleshooting.
Installation
dotnet add package KyaOs.CheckpointNuGet resolves the right adapter for your project's target framework automatically.
Quick Setup
Set credentials in appsettings.json:
{
"Checkpoint": {
"ProjectId": "your_project_id",
"ApiKey": "your_api_key"
}
}Then register in Program.cs, which reads those two settings:
using Checkpoint.AspNetCore.Extensions;
using Checkpoint.Core.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCheckpoint(options =>
{
options.ProjectId = builder.Configuration["Checkpoint:ProjectId"];
options.ApiKey = builder.Configuration["Checkpoint:ApiKey"];
options.OnAgentDetected = DetectedAction.Block;
});
var app = builder.Build();
app.UseCheckpoint(); // before routing + auth
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();If either setting is missing, the app still starts, in local detection-only mode: detection and your OnAgentDetected action run, but no dashboard policy applies and no detections reach the dashboard. Vouched KYA logs one warning at startup that names the missing setting. An ApiKey without a ProjectId does nothing, so that case is logged as an error. An empty value counts as missing.
Deploy. Vouched KYA runs on every request.
Configuration
builder.Services.AddCheckpoint(options =>
{
options.ProjectId = "your_project_id";
options.ApiKey = "your_api_key";
options.OnAgentDetected = DetectedAction.Block;
options.ConfidenceThreshold = 70;
options.SkipPaths = ["/health", "/api/webhooks/*"];
options.IncludePaths = ["/api/*"];
options.McpServerUrl = "https://mcp.example.com";
options.SiteName = "My Website";
options.BlockedResponse = new BlockedResponseOptions
{
StatusCode = 403,
Message = "Access denied: AI agent detected",
};
});Detection Actions
Path Filtering
IncludePaths takes priority: if set, only matching paths are processed. Both support wildcard suffixes (/api/*).
Enforce vs Observe Mode
Vouched KYA for .NET picks observe or enforce with OnAgentDetected. DetectedAction.Log is observe mode and the default, so a fresh install starts in observe mode: every request is still classified, but only logged and reported to the dashboard as log, and the SDK blocks nothing on its own. Once ProjectId and ApiKey are set, the project's dashboard policy still applies: an agent on its deny list is blocked even under Log, so keep the deny list empty while you observe. DetectedAction.Allow also passes every request through, but reports as allow (an enforce-mode permit) rather than an observe-mode log. Block, Redirect, Challenge, and Instruct are the enforcement actions.
A request that presents a KYA-OS HTTP signature is refused in every mode, observe included, when the SDK can't verify the signature. That covers a signed body over 1 MiB (see Signature Verification). Java's observe mode passes such a request through and reports it instead; .NET has no equivalent.
Access Detection Results
Vouched KYA attaches its detection result to each request. Read it in controllers or downstream middleware with GetCheckpointResult(), not by its HttpContext.Items key: the key is an internal name and can change between releases.
using Checkpoint.AspNetCore;
public IActionResult Get()
{
var result = HttpContext.GetCheckpointResult();
if (result?.IsAgent == true)
{
return Ok(new { agent = result.DetectedAgent?.Name, confidence = result.Confidence });
}
return Ok(new { message = "Hello human!" });
}GetCheckpointResult() returns null when the middleware produced no verdict for the request: it is not in the pipeline, or it failed open on an error before reaching a verdict. On a path the middleware skips (outside IncludePaths, or in SkipPaths) it returns a placeholder human result instead, and WasCheckpointSkipped() returns true; check it before you gate anything on the result. GetCheckpointSessionId() returns the session id the middleware resolved.
Read the browser posture outcome
With browser posture configured, the middleware or module verifies the posture token once per request. A handler reads that outcome instead of verifying the token again. GetCheckpointPostureOutcome() returns a PostureOutcome? (namespace Checkpoint.Core.Http): StatusName is verified, absent, or rejected, ReasonName names why a token was rejected, and Posture holds the verified claims when the status is verified.
using Checkpoint.AspNetCore;
app.MapGet("/checkout", (HttpContext context) =>
{
var outcome = context.GetCheckpointPostureOutcome();
if (outcome is null)
{
return Results.Ok(new { posture = "unknown" });
}
return Results.Ok(new
{
posture = outcome.StatusName,
reason = outcome.ReasonName,
verdict = outcome.Posture?.Verdict,
});
});The extension class is CheckpointAspNetCoreHttpContextExtensions. Its name differs from the classic CheckpointHttpContextExtensions so both packages can be imported in one file.
null means the request has no verdict: the middleware or module skipped it, didn't run, or browser posture isn't configured. It isn't the same as absent, which means the request carried no token. A handler that gates an action on posture should treat null as unknown, not as absent. A rejected or absent token doesn't stop the request, so the handler decides what to do with it.
A token can also carry a detection claim: the beacon's own final verdict, { class: "ai_agent" | "bot", name, confidence }, exposed as Posture.Detection (Class, Name, Confidence). When the baseline detection is human or incomplete data, the claim classifies the request as that class with that name and confidence (an AI agent or bot, IsAgent true), and it wins over the automation flags and the agent-driven verdict. An existing agent or bot verdict is never downgraded. A present but malformed claim (a class other than ai_agent or bot, a name outside 1 to 64 characters or with control characters, a confidence that is not an integer from 0 to 100) rejects the whole token with ReasonName invalid-claims. A disclosed navigator.webdriver now also sets IsAgent true on its Bot classification, matching the TypeScript SDK. The detection claim and a disclosed navigator.webdriver set IsAgent true, so your configured OnAgentDetected action applies to them when the confidence meets ConfidenceThreshold. Corroborated-only automation evidence classifies the request as incomplete data with IsAgent false, so it never enforces. The agent-driven verdict never runs the configured action; it is reported and can only trigger a browser integrity step-up.
Response Headers
Every response includes detection metadata:
KYA-Verification names the method behind the classification: Pattern, Signature, Behavioral, Network, KyaHttp, KyaHttpDelegated, McpIHandshake or A2A.
Signature Verification
Ed25519 signature verification for ChatGPT (RFC 9421) is on by default. Verified agents score at the top of the confidence range. ChatGPT's public key refreshes automatically from OpenAI's JWKS.
Setting EnableSignatureVerification to false turns off the ChatGPT check only. KYA-OS signatures are verified either way.
To verify a KYA-OS signature, the SDK reads the request body and checks it against the signed Content-Digest, then hands your app the same bytes. A signed body may be up to 1 MiB. A larger one is answered 413 in every mode. When Content-Length declares the size, the SDK answers before reading any of the body. A chunked body is read only up to the limit.
options.EnableSignatureVerification = false;KYA-OS Instruct Mode
DetectedAction.Instruct returns a KYA-OS challenge carrying a machine-readable mcp_i JSON body and a plain-language message the agent can relay to its user.
.NET writes HTTP 200 for Instruct, unconditionally. Unlike the Node SDKs it does not
negotiate an envelope for this action: DelegationChallengeMode is honoured for Challenge only.
A client that treats 2xx as success will read every instruct response as a successful request, so
branch on the mcp_i body or the KYA-Action: instruct header, never on the status code. See
INSTRUCT by surface for how the other four surfaces
differ.
Both runtimes send the same response. message and user_action_required are plain text an AI assistant can relay to its user. mcp_i is for clients that drive the flow themselves: send the user to authorization_url to grant consent, then retry the request with the delegation credential in the KYA-Delegation header.
For example, ChatGPT asks a site whose ProjectId is your_project_id for a page:
GET /products HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://chatgpt.com/bot)Vouched KYA answers in place of the page:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
KYA-Action: instruct
KYA-Auth-Required: true
KYA-Auth-Url: https://kya.vouched.id/connect/your_project_id?agent=chatgpt
KYA-Project: your_project_id{
"message": "This website requires AI agent verification. To give me access, please visit: ...",
"user_action_required": { "action": "Set up the MCP connection for this website", "url": "..." },
"mcp_i": {
"version": "1.0",
"action": "authenticate",
"authorization_url": "https://kya.vouched.id/connect/your_project_id?agent=chatgpt",
"project_id": "your_project_id",
"required_scopes": ["api:read"],
"retry_instructions": {
"header": "KYA-Delegation",
"format": "JWT delegation credential from authorization flow"
}
},
"error": "mcp_authentication_required",
"code": "AGENT_REQUIRES_DELEGATION"
}The response is abbreviated: it leaves out the Link header, the detection headers listed under Response Headers, and some body fields, among them mcp_i.flow, which lists the same steps in words. When a dashboard policy sets a redirect URL, authorization_url is that URL with ?agent= added. Otherwise it is your project's connect page, {BaseUrl}/connect/{ProjectId}?agent=<agent name, lowercased>, where the user installs the connection for their assistant.
Once the user has granted consent, the agent sends the same request again with the delegation credential:
GET /products HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://chatgpt.com/bot)
KYA-Delegation: <delegation credential from the authorization flow>To turn it on:
options.OnAgentDetected = DetectedAction.Instruct;
options.McpServerUrl = "https://mcp.example.com";Native HTTP Mode
In native mode the module registers each challenge with Vouched KYA, which relays it to your project's authorization host. Without the native settings, the module registers challenges directly with the authorization host, using your site's API key.
Native mode arrived in 1.10.0. Use the current release, 1.15.0.
options.HttpResourceUri = "https://www.example.com";
options.HttpResourceDid = "did:web:www.example.com";
options.HttpChallengeSigningDids = ["did:web:kya.vouched.id:orgs:acme-corp"];
options.TrustedDelegationRoots = ["did:web:kya.vouched.id:orgs:acme-corp"];- The organization DID comes from your organization's slug, not its display name. Renaming the
organization in the dashboard leaves the slug and the DID unchanged. If the slug itself changes,
so does the DID. Vouched KYA has done this once, to remove generated suffixes such as
-abc123. The old DID keeps resolving, but new challenges are signed under the new one, and the module refuses them until you updateHttpChallengeSigningDidsandTrustedDelegationRootson every site. If you use the shopper helper below, updateCustomerRoottoo: with the old DID, every shopper is served as a guest, with no error. To check the value, openhttps://kya.vouched.id/orgs/<org-slug>/did.json: itsidis the DID to use. - The module fetches the DID documents and status lists a credential needs, in the background.
A request never waits on a fetch: one not fetched yet is answered 503
kyaos/identity-pendingorkyaos/status-pendingwithRetry-After: 1, and the client's retry usually verifies. Fetches are HTTPS on port 443 to public addresses only, so the server needs outbound HTTPS tokya.vouched.idand to thedid:webhosts of the agents you accept. FailOpendefaults tofalse. Native authorization already fails closed on a missing or invalid credential. With native mode on andFailOpen = false, the module doesn't fall back to its last cached policy: every request, humans included, gets a 503kyaos/policy-unavailablewhenever the policy can't be fetched. SetFailOpen = trueto fall back to the cached policy.- Leave
AuthorizationHostUrlas it is. In native mode the module registers each challenge with Vouched KYA (BaseUrl,https://kya.vouched.idby default) and takes the consent link from that registration. The authorization host in your published policy wins over this setting; one of the two only has to be present for registration to run. - Sign-in callback. If your site is also the sign-in provider for consent, its OAuth app needs
the authorization host's callback among its allowed redirect URIs. When consent runs on
Vouched KYA, it's
https://kya.vouched.id/api/consent/oauth/callback. The/api/v1/bouncer/oauth/callbackin the Govern docs is for GitHub- or Google-style providers, not your own sign-in. - Confirm it took effect. After deploying, open
https://www.example.com/.well-known/oauth-protected-resource.resourceshould be yourHttpResourceUri, andkyaos_challenge_signing_didsshould list your organization DID. If the document has nokyaos_challenge_signing_dids, the site isn't running native mode.
Act as the Shopper on a Delegated Request
Once a shopper approves an agent, the agent retries with a delegation credential and the module verifies it. That request still arrives without the shopper's cookies, so a page that builds the cart or the account from the session sees a guest. To serve the shopper's own cart, read who approved the grant from the detection result and load that customer.
The module sets DelegationHolder on the result only after it has verified a delegation credential
(VerificationMethod is KyaHttpDelegated). It is null on every other request.
When <id> is your customer id. It is the id your own sign-in returned for that shopper when
the shopper signed in on the consent page with your sign-in: your OAuth provider or a merchant
credential, set under Policy โ Auth. For an OAuth provider that is the userinfo field the
provider's user id mapping names (userInfoFields.userId), or the first of sub, id and
customerId when no mapping is set. With
Vouched KYA's sign-in methods (email code, passkey, password, a Vouched KYA account) it is an id
Vouched KYA assigned, not one of your customers. A Subject that is a did:key has no customer id at
all: it comes from a grant another authorization host issued, so treat that request as a guest or
refuse it.
To act as the shopper:
- Check that
VerificationMethodisKyaHttpDelegatedand that the policy didn't block the request (ShouldBlockis nottrue). - Check that
GrantedScopesincludes the scope this endpoint needs, and refuse with 403 when it doesn't. Each endpoint names its own scope:cart:readto view the cart,cart:writeto add to it. A grant to view the cart can't add to it. - Read
DelegationHolder.CustomerId, and check thatSubjectis exactly<your organization's DID>:users:<CustomerId>.CustomerIdis set for a grant from any of yourTrustedDelegationRoots, and it doesn't say which: user 42 from each root gives the sameCustomerId.Subjecttells them apart.null, or aSubjectthat doesn't match, means no customer: don't act as anyone. - Load that customer's cart instead of the session's.
The helper below does the first three steps, so every endpoint makes one call with its own scope.
using Checkpoint.AspNetCore;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
private const string CustomerRoot = "did:web:kya.vouched.id:orgs:acme-corp";
public IActionResult Cart()
{
var access = DelegatedShopper.Check(
HttpContext.GetCheckpointResult(), CustomerRoot, "cart:read", out var customerId);
if (access == ShopperAccess.Forbidden)
{
return StatusCode(StatusCodes.Status403Forbidden);
}
return View(access == ShopperAccess.Shopper
? _carts.ForCustomer(customerId)
: _carts.ForSession(HttpContext));
}
[HttpPost]
public IActionResult AddToCart(string sku)
{
var access = DelegatedShopper.Check(
HttpContext.GetCheckpointResult(), CustomerRoot, "cart:write", out var customerId);
if (access == ShopperAccess.Forbidden)
{
return StatusCode(StatusCodes.Status403Forbidden);
}
var cart = access == ShopperAccess.Shopper
? _carts.ForCustomer(customerId)
: _carts.ForSession(HttpContext);
cart.Add(sku);
return RedirectToAction(nameof(Cart));
}Both use the same helper. Pass your organization's DID, the same value as in
TrustedDelegationRoots, and the scope the endpoint needs. It returns:
Forbiddenfor a verified grant that doesn't include the scope, whoever it comes from, so the endpoint refuses.Shopperwith the customer id for a verified grant that has the scope and comes from a user of the organization you passed.Guestfor everything else: no verified grant, a request the policy blocked, or a grant that has the scope but no customer id from that organization (adid:key, another host's or organization's DID, a user of another trusted root, extra segments, or noTrustedDelegationRootsconfigured). Serve the session's own cart, as for any visitor.
using System.Linq;
using Checkpoint.Core.Models;
public enum ShopperAccess
{
Guest,
Shopper,
Forbidden,
}
public static class DelegatedShopper
{
public static ShopperAccess Check(
DetectionResult result, string trustedRoot, string requiredScope, out string customerId)
{
customerId = null;
if (result == null ||
result.VerificationMethod != VerificationMethod.KyaHttpDelegated ||
result.ShouldBlock == true)
{
return ShopperAccess.Guest;
}
if (result.GrantedScopes == null || !result.GrantedScopes.Contains(requiredScope))
{
return ShopperAccess.Forbidden;
}
// CustomerId doesn't name its root, so user 42 from a second root would match too.
var holder = result.DelegationHolder;
if (holder?.CustomerId == null || holder.Subject != trustedRoot + ":users:" + holder.CustomerId)
{
return ShopperAccess.Guest;
}
customerId = holder.CustomerId;
return ShopperAccess.Shopper;
}
}- Name the scope per endpoint. Copying the cart page's call to a new endpoint without changing the scope gives that endpoint the cart page's access. Use the scope your consent screen asks the shopper to approve for that action.
- Look the id up, don't trust it as a key blindly. Resolve it to your customer through your own lookup, and fall back to the guest experience when no customer matches.
- Never read the credential yourself. The module has already verified the signature; decoding the Bearer token in your own code to make a decision skips that check.
Dashboard Reporting
Detection events flow to your Vouched KYA dashboard automatically: every AI agent and bot classified above the confidence threshold is reported fire-and-forget after the request completes. Pure human traffic is not reported. Same payload, same gating, and same dashboard experience across ASP.NET Core and .NET Framework.
Requires Checkpoint:ProjectId and Checkpoint:ApiKey to be set. Without them, detection and enforcement still run: only the dashboard hop is skipped.
Upgrading
Already running Vouched KYA and want to move to the latest release? The path
is the same across runtimes: bump the package version, restore, redeploy.
Some 1.x releases change behavior or remove public types, so read
Breaking Changes Since 1.12.0 first.
The current stable release is 1.15.0.
# If you installed the metapackage (recommended)
dotnet add package KyaOs.Checkpoint --version 1.15.0
# If you installed the adapter package directly
dotnet add package Checkpoint.AspNetCore --version 1.15.0After the bump: dotnet restore, rebuild, redeploy.
Binding Redirects on .NET Framework
A release can move a dependency such as System.Text.Json to a new
assembly version, so check the redirects in Web.config on every upgrade.
The package ships the set it needs in web.config.install.xdt
(packages/Checkpoint.AspNet.<version>/content/ for packages.config,
~/.nuget/packages/checkpoint.aspnet/<version>/contentFiles/any/net462/
for PackageReference). NuGet inserts that set only when Web.config has no
<runtime> block, so it never updates redirects you already have.
For each assembly in that file, set your bindingRedirect to its
oldVersion and newVersion, or add the entry if it's missing. Visual
Studio's Package Manager usually rewrites redirects during Update-Package
(unless nuget.config sets bindingRedirects to skip); editing files by
hand or with another tool doesn't. A redirect that doesn't match the DLL in
bin/ fails every request with
FileLoadException 0x80131040.
Should I Switch to the Metapackage?
If your csproj references Checkpoint.AspNetCore or Checkpoint.AspNet
directly, both packages remain first-class and continue to ship at the same
version cadence. You do not have to switch. Switching to KyaOs.Checkpoint
is purely a discoverability and forward-compatibility convenience:
- One install command works for any target framework (NuGet picks the adapter for you).
- New runtimes added to the metapackage in the future are picked up automatically.
- Namespaces and API are identical:
using Checkpoint.AspNetCore.Extensions;andAddCheckpoint(...)continue to compile unchanged.
To switch, swap the package reference and rebuild: no source edits.
Breaking Change to Know About: Response Headers Renamed in 0.4.0
If you are upgrading from 0.3.x or earlier to 0.4.0 or later (which
includes any move to a 1.x release), the custom response headers were
renamed per RFC 6648 (deprecating the X- prefix) and the KYA-OS rebrand.
The middleware behavior is identical: only the header names changed.
If you have downstream code, log queries, alerts, WAF rules, or monitoring dashboards keyed off the old names, search-and-replace before deploying.
Breaking Changes Since 1.12.0
Crossing one of these versions can change what your site does. Move every Checkpoint package you reference to the same version. The changelog has the full list.
1.12.0
- A request with no evidence either way reports
IncompleteData, notHuman.IsAgentstays false and composed rules still read it as human, butKYA-Class,DetectionClassand the class sent to the dashboard change. Check any code or report that reads them. - Protected-resource metadata names the authorization server the platform sends, and answers 404 when it sends none.
BuildDocument,BuildNativeDocumentandBuildAccessGuideonProtectedResourceMetadataBuildertake the platform's issuer as a parameter. Code that calls the old signatures no longer compiles.
1.13.0
-
FailOpendefaults tofalse. It wastruethrough 1.12.x. A site with noFailOpensetting (Web.configCheckpoint:FailOpen) goes from fail-open to fail-closed on this upgrade. Choose the value on purpose before you upgrade:false: an SDK error blocks the request with 503 andRetry-After: 3. A detection outage counts as an SDK error: on default options, a 32-bit app pool or a missingwasmtime.dllgets the same 503 on every request the module covers, until the host is fixed. A failed policy fetch with no cached policy answers every request it covers the same way, humans included. The cache is in memory, so an app pool recycle during an outage is enough. With a cached policy, the site keeps using it. In native mode the answer is 503kyaos/policy-unavailable, and the cached policy is not used.true: an SDK error lets the request through withKYA-Failopen, and a failed policy fetch falls back to the cached policy. With no cached policy, no policy rule applies, so a path you protect is not protected until the fetch recovers. A detection outage or SDK error is the exception: withOnAgentDetectedset toBlock,Redirect,ChallengeorInstruct, every affected request the module covers gets that action unlessOnDetectionUnavailableisAllow.
IncludePathslimits which paths this can reach, because the module doesn't run on any other path. Those paths also get no detection. -
A 200 from
/api/internal/policies/{id}with noblockThresholdis treated as a failed fetch. Checkpoint's route always sends it. A proxy or test double standing in for that route has to send it too. -
A presented credential that can't be verified returns 503 even with
FailOpen = true. One the engine refuses keeps its 401 or 403. -
A Cedar error on an enforcing project blocks the request, unless
FailOpenistrue. -
WebMcpReadOnlyOptions.StatusListUrl,SignedStatusListVerifierandStatusList2021are gone. TheStatusListResolverconstructor changed, andStatusListResult.ExpiresAtUnixis no longer nullable.
1.14.0
- The embedded engine refuses MCP-I envelopes and the legacy Layer 2 delegation
headers outside the native propagation lane with 403
kyaos/unsupported-carriage. - The SDK makes outbound HTTPS requests to
feeds.kya-os.ai. With an egress allow-list, allow that host, setVendorKeyFeedUrlto a mirror, or setEnableVendorKeyFeed = false. HttpProofClaim.ResourceDidis nowHttpProofClaim.Scope, and some unused public types are gone fromCheckpoint.Core.
1.15.0
- Servers need outbound HTTPS to the trusted roots and to the
did:webhosts of the agents and issuers they accept. - A status list must be hosted on its issuer's
did:webhost. - An engine block is answered as the engine renders it.
BlockedResponseapplies only to blocks the SDK decides. - The public resolution types are gone. Use
ResolutionWarmer.CreateDefault. VerificationMethod.McpIHandshakeandAuthorizeInputProjection.IsUnverifiedOptimisticProtocolBlockare gone.
Verifying the Upgrade
After the redeploy, load a page as a person first. A FileLoadException
(0x80131040) on the first request means a binding redirect is missing or doesn't
match a DLL. See
FileLoadException with HRESULT 0x80131040.
Then run these checks:
# 1. Confirm the new header set is present
curl -sI https://your-site.example.com/ -A "GPTBot/1.0" \
| grep -i "^KYA-"
# Expect KYA-Detected, KYA-Confidence, KYA-Class, KYA-Agent at minimum.# 2. Confirm classification is correct (a known bot should report bot, not ai_agent)
curl -sI https://your-site.example.com/ -A "Googlebot/2.1 (+http://www.google.com/bot.html)" \
| grep -i "^KYA-Class"
# Expect: KYA-Class: Bot# 3. Confirm dashboard rows show the correct detection class
# In your Checkpoint dashboard, filter the Detections table by
# source=middleware and detectionClass=bot. The Googlebot probe from
# step 2 should appear there within ~30 seconds.# 4. If you enforce, confirm a protected path answers as it did before
curl -s -o /dev/null -D - -A "Claude-User/1.0" https://your-site.example.com/your-protected-path \
| grep -iE "^HTTP|^KYA-Action"
# Expect the same status and KYA-Action as before the upgrade, unless a listed
# breaking change covers it.# 5. In native mode, confirm the protected-resource metadata still answers
curl -s https://your-site.example.com/.well-known/oauth-protected-resource
# Expect a 200 with your resource. From 1.12.0, authorization_servers names
# the issuer the platform sends, so that value can change on this upgrade.If every page answers 503, read the body and the response headers first.
- The generic body with
KYA-Detection: unavailable: detection can't run. The body is{"error":"Service temporarily unavailable","source":"checkpoint-middleware"}withRetry-After: 3, and it lasts until the host is fixed. On .NET Framework, check thatwasmtime.dllis inbin/and that the app pool is 64-bit. See a 32-bit host and a missing wasmtime.dll. - The generic body without that header: the site can't read its policy and
FailOpenisfalse, or another SDK error failed closed. CheckProjectId,ApiKey, and outbound HTTPS from the server toBaseUrl. kyaos/policy-unavailable: a native site that can't read its policy, or a native site with an enforcing policy where detection can't run. This 503 carries noKYA-Detectionheader, so use the trace log:Checkpoint startup phase=DetectorWarmupwithoutcome=Unavailablemeans detection. Checkwasmtime.dlland the app pool first, thenProjectId,ApiKey, and outbound HTTPS.
If KYA-_ headers are absent but X-Checkpoint-_ are still present, the new
package did not load: check that the restore ran and that the build
output's Checkpoint.Core.dll reports the new version
(AssemblyInformationalVersion matches 1.15.0).
If KYA-Class reports AiAgent for known crawlers like Googlebot, the
embedded engine is stale: confirm Checkpoint.Core resolved to 1.15.0
in obj/project.assets.json, or in packages.config on a site that uses it. The dependabot lockstep groups in the
demo repo can be used as a reference for keeping Checkpoint.* packages
on a single version.
Troubleshooting
The errors below are specific to the .NET Framework adapter. Modern ASP.NET Core installs don't hit any of them.
Detection Unavailable on a 32-Bit Host
Symptom. The site starts, but detection can't run. From 1.13.0, on default options, every page the module covers answers 503 and no detections reach the dashboard. The trace log shows Checkpoint startup phase=DetectorWarmup with outcome=Unavailable. Most sites get the generic 503: it has Retry-After: 3 and KYA-Detection: unavailable, and each request logs Checkpoint module error (fail-closed, blocking). A native site with an enforcing policy answers kyaos/policy-unavailable instead, with neither that header nor that log line.
Cause. The app pool is running in 32-bit mode. Vouched KYA's WebAssembly runtime ships x64 only and has no 32-bit fallback, so detection can't run until the host is fixed. From 1.13.0, your settings decide what answers:
- Default options: 503 on every page the module covers.
OnAgentDetectedset toBlock,Redirect,ChallengeorInstruct: that action answers every page the module covers, people included, whateverFailOpensays, unlessOnDetectionUnavailableisAllow.FailOpenset totruewithOnAgentDetectedleft onLog, orCheckpoint:OnDetectionUnavailableset toAllow: requests pass through with nothing enforced. A request that presents a credential doesn't pass through.
One exception: a native site with an enforcing policy and FailOpen set to false answers 503 kyaos/policy-unavailable whatever those settings say.
Fix.
- Azure App Service. Upgrade the App Service Plan from Free (F1) or Shared (D1) to Basic (B1) or higher. In the portal, open Configuration โ General Settings, set Platform to 64-bit, and restart the app.
- IIS on Windows Server. In IIS Manager, open Application Pools โ your pool โ Advanced Settings and set Enable 32-Bit Applications to
False. - Standalone host. Add
<PlatformTarget>x64</PlatformTarget>to your csproj, orAnyCPUwith<Prefer32Bit>false</Prefer32Bit>.
FileLoadException with HRESULT 0x80131040
Symptom. First request returns a 500, with an error like:
Could not load file or assembly 'Microsoft.Extensions.Logging.Abstractions'
or one of its dependencies. (0x80131040)Cause. Classic ASP.NET needs binding redirects in Web.config for the transitive Microsoft.Extensions.* and System.Text.Json dependency graph. Checkpoint.AspNet 0.2.1+ ships an auto-generated redirect set inside the NuGet package, but it's applied differently depending on whether your project uses packages.config or PackageReference.
Fix for packages.config. On a first install into a Web.config with no <runtime> block, NuGet inserts the shipped set for you. If Web.config already had a <runtime> block, or after an upgrade, update the redirects from the shipped file as described in Binding redirects on .NET Framework:
packages/Checkpoint.AspNet.<version>/content/web.config.install.xdtFix for PackageReference. NuGet does not auto-transform content files for PackageReference projects. Copy the shipped redirect set into Web.config: open the XDT at the path below and merge its <runtime> block into yours.
~/.nuget/packages/checkpoint.aspnet/<version>/contentFiles/any/net462/web.config.install.xdtAutoGenerateBindingRedirects doesn't fix this on its own for a web project: MSBuild writes the generated redirects to bin/<YourApp>.dll.config, which IIS doesn't read. Web.config must carry them.
wasmtime.dll Missing From bin
Symptom. The same as a 32-bit host: from 1.13.0, on default options, every page the module covers answers 503. The app pool is 64-bit, but wasmtime.dll isn't in bin/.
Cause. The NuGet consumer-side targets file that copies wasmtime.dll into your build output didn't fire. As of Checkpoint.Core 0.2.2+, the file ships at buildTransitive/net462/ and is imported automatically by both direct and transitive PackageReference consumers as well as packages.config consumers.
Fix.
- Install
KyaOs.CheckpointorCheckpoint.AspNet, not bareCheckpoint.Core. The targets file lives in the Framework adapter's transitive graph; a project that references onlyCheckpoint.Coredirectly won't pull it in. - On a
packages.configproject, check the<Import>forCheckpoint.Core.targetsin your.csproj. It names a package version, and it's what copieswasmtime.dllintobin/. It must name the version you installed. - If the file is still missing, run
msbuild /verbosity:detailedand search the log forCheckpoint.Core.targets. A missing import means NuGet's targets graph isn't being walked by your project type: email kya+support@vouched.id with a minimal repro.
Supported Versions
Since 1.11.9, the ASP.NET Core adapter ships two builds, net8.0 and net10.0. NuGet picks net10.0 for a .NET 10 app and net8.0 for a .NET 8 or 9 app, on its own. The adapter's tests run on .NET 8 and on .NET 10. .NET 6 and 7 are not supported, because the package has no build they can load.
Microsoft ends support for .NET 8 and .NET 9 on November 10, 2026. .NET 10 is the current long-term support release, supported until November 2028, so build new apps on it and plan upgrades for older ones.
.NET Framework requires a 64-bit host process (Windows Server IIS or Azure App Service B1+).