@astrasyncai/verification-gateway
v5.16.0
Published
AstraSync KYA Platform SDK — counterparty verification gateway (verify incoming requests) + agent registration (register AI agents with the KYA backend).
Readme
@astrasyncai/verification-gateway
The AstraSync KYA Platform SDK. One package, two roles: register agents with the KYA backend, and verify agents at any counterparty type (API / MCP / website / agent-to-agent).
What's in this package?
As of v2.4.0 this is the only npm package you need for AstraSync integration. Pick the subpath that matches what you're doing:
| If you're… | Import from | Get |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Registering an agent with the KYA backend | @astrasyncai/verification-gateway/registration | AstraSync class — register(), verify(), health(). The astrasync CLI is bundled too. |
| Building an agent that calls out to other services (per-request credential injection) | @astrasyncai/verification-gateway/agent | AgentClient, ChallengeHandler, ownership validation, runtime PDLSS shape. |
| Running an Express API that AI agents call into | @astrasyncai/verification-gateway/express | createMiddleware() — protect routes, attach verification context. |
| Running a Next.js app that AI agents call into | @astrasyncai/verification-gateway/nextjs | createMiddleware(), verification-interstitial wiring. |
| Running an MCP server that AI agents call into | @astrasyncai/verification-gateway/mcp | MCP tool gates + inner-hop dedupe headers. |
| Doing direct verification (no framework) | @astrasyncai/verification-gateway/sdk | VerificationGatewayClient with retry / backoff / timeout. |
| Parsing protocol-level credentials (HTTP / A2A / MCP / x402 / AP2 / MPP) | @astrasyncai/verification-gateway/transport | Cross-protocol credential extraction + injection. |
| Evaluating PDLSS offline (local / hybrid mode) | @astrasyncai/verification-gateway/gateway | AstraSyncGateway (online / local / hybrid). |
| Governing Claude Code with a local policy | @astrasyncai/verification-gateway/claude-code | PreToolUse-hook adapter; astrasync-guard install-claude-code sets it up. |
| Receiving webhook callbacks | @astrasyncai/verification-gateway/webhooks | verifyAstraSyncWebhook(), signAstraSyncWebhook(). |
| Building on the Trusted Agent Gateway edge machinery | .../edge-config, .../platform-signatures, .../metadata-capture | Dashboard-managed edge policy, platform fingerprint registry, shared header sanitiser — see Metadata capture. |
All subpaths talk to the same backend —
POST /agents/verify-accessfor verification,POST /agents/registerfor registration. The contract is shared; the role is just which side of the handshake you're sitting on.
Before v2.4.0 the agent-registration half shipped as a separate package (@astrasyncai/agent-registration, briefly; @astrasyncai/sdk before that). It was merged in to stop the two-package version drift and to give partners one install instead of two. The legacy @astrasyncai/sdk package on npm is deprecated with a pointer to this package; @astrasyncai/agent-registration was never published.
Overview
The Verification Gateway provides a single, universal solution for verifying AI agents. One codebase, multiple deployment targets:
- Express.js middleware - Protect API endpoints
- Next.js middleware - Protect web applications with the verification interstitial
- SDK functions - Direct verification for agent-to-agent or serverless
All verification flows through the same POST /agents/verify-access endpoint, ensuring consistent PDLSS (Permission, Duration, Limit, Scope, Self-instantiation) enforcement.
Installation
npm install @astrasyncai/verification-gatewayQuick Start
Express Middleware
import express from 'express';
import { createMiddleware } from '@astrasyncai/verification-gateway/express';
const app = express();
// v2.3.7+ — per-route policy lives in the AstraSync dashboard.
// The SDK fetches it on init via counterpartyId; do NOT pass routes here.
app.use(
createMiddleware({
apiBaseUrl: 'https://astrasync.ai/api',
apiKey: process.env.ASTRASYNC_API_KEY,
counterpartyId: 'ASTRAE-...', // your endpoint id from the dashboard
setPassThroughHeader: true, // recommended for local-dev / staging
// — surfaces pass-through mode when
// no per-route policy is configured
})
);Next.js Middleware
// middleware.ts
import { createMiddleware } from '@astrasyncai/verification-gateway/nextjs';
// v2.3.7+ — per-route policy lives in the AstraSync dashboard.
// The SDK fetches it on init via counterpartyId; do NOT pass routes here.
export const middleware = createMiddleware({
apiBaseUrl: 'https://api.astrasync.ai',
apiKey: process.env.ASTRASYNC_API_KEY,
counterpartyId: 'ASTRAE-...',
showInterstitial: true,
});
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*'],
};SDK (Direct Usage)
import { createClient } from '@astrasyncai/verification-gateway/sdk';
const gateway = createClient({
apiBaseUrl: 'https://api.astrasync.ai',
});
// Verify another agent before interacting
const result = await gateway.verify({
astraId: 'ASTRA-abc123',
purpose: 'data-exchange',
});
// 5.0.0: the accessLevel band is gone — the decision is the two axes.
if (result.identityVerified && result.policyAllowed) {
// Safe to interact with this agent
console.log(`Trust score: ${result.agent?.trustScore}`);
}Agent Registration
Use the SDK for all agent registration, whether you're a developer firing off a one-shot CLI call or an autonomous agent self-registering on first run. The SDK handles auth-mode routing for you: signature-authenticated callers get synchronous registration; API-key-only callers get an owner-approval handshake (server-side rule, not negotiable).
apiEndpoint — runtime-challenge URL
apiEndpoint is the URL where your agent's verification-gateway SDK is mounted to receive runtime challenges from counterparties. Optional but recommended — if you omit it, your agent declares no runtime-challenge support, which is included in the verification payload and may cause some counterparties to decline access requests.
Two registration response modes
The same register() call returns one of two shapes depending on auth context:
- 201 active — synchronous: returned when the request is signed with a crypto keypair (
privateKeyconfigured) or when authenticated via email+password. Result:{ status: 'active', agent }. - 202 pending_approval — API-key only: the SDK was authenticated with an API key but the request was not signed. The backend emails the account owner a "Sign In to Accept" link, fires a dashboard alert, and returns a tracking token. Result:
{ status: 'pending_approval', requestId, pollUrl, expiresAt }. The agent becomes active only after the owner approves.
This split enforces the platform rule that API-key registrations always require owner step-up — registering an agent autonomously with just an API key is not a sufficient authority signal on its own.
Pattern A — developer / long-running agent (block until approved)
Best for CLI tools, dev-machine first-run, and long-running services:
import {
AstraSync,
RegistrationDeniedError,
RegistrationTimeoutError,
} from '@astrasyncai/verification-gateway/registration';
const sdk = new AstraSync({
apiKey: process.env.ASTRASYNC_API_KEY,
privateKey: process.env.ASTRASYNC_PRIVATE_KEY, // optional — when set, 201 sync path
});
try {
const agent = await sdk.register({
name: 'invoice-bot',
description: 'Reconciles AP/AR across accounting systems.',
agentType: 'autonomous',
apiEndpoint: 'https://invoice-bot.example.com', // runtime-challenge URL
model: { modelProvider: 'anthropic', modelName: 'claude-sonnet-4-5' },
framework: { frameworkName: 'langchain', frameworkVersion: '0.3.0' },
protocols: ['a2a', 'mcp'],
pdlss: {
purpose: {
categories: ['accounting'],
allowedActions: ['accounting.read', 'accounting.write'],
},
limits: { autonomousThreshold: 1_000, approvalThreshold: 10_000, currency: 'USD' },
scope: { resources: ['xero.invoices', 'quickbooks.bills'] },
selfInstantiation: { allowed: false },
},
// Blocking mode: poll until the request resolves.
waitForApproval: true,
timeoutMs: 10 * 60 * 1000, // 10 minutes default
onPending: ({ ageMs }) =>
console.log(`Awaiting owner approval (${(ageMs / 1000).toFixed(0)}s)…`),
});
console.log(`Agent registered: ${agent.astrasyncIdLevel1}`);
} catch (err) {
if (err instanceof RegistrationDeniedError) {
console.error('Owner denied:', err.reason);
} else if (err instanceof RegistrationTimeoutError) {
console.error(
'Timed out — request is still active server-side; poll later via sdk.waitForApproval(requestId)'
);
} else {
throw err;
}
}Pattern B — serverless / scheduled agent (non-blocking, exit and resume)
Best for Lambda / Cloud Functions / cron-driven self-registration where you can't hold the runtime open for minutes:
const sdk = new AstraSync({ apiKey: process.env.ASTRASYNC_API_KEY });
const result = await sdk.register({
name: 'invoice-bot',
apiEndpoint: 'https://invoice-bot.example.com',
pdlss: { purpose: { categories: ['accounting'], allowedActions: ['accounting.read'] } },
});
if (result.status === 'pending_approval') {
// Store the requestId in your durable storage (DynamoDB, KV, etc.)
await store.set('astrasync.pendingRequestId', result.requestId);
console.log(`Awaiting owner approval at ${result.pollUrl}; will resume on next scheduled run.`);
return; // function exits — owner has time to approve
}
console.log(`Agent registered as ${result.agent.astrasyncIdLevel1}`);On the next scheduled run, resume by polling:
const requestId = await store.get('astrasync.pendingRequestId');
const status = await sdk.pollRegistration(requestId);
if (status.state === 'approved') {
await store.set('astrasync.agentId', status.agent.kyaAgentId);
await store.delete('astrasync.pendingRequestId');
} else if (status.state === 'denied' || status.state === 'expired') {
console.error(`Registration terminated: ${status.state}`);
}CLI equivalent — same surface from a shell, also via npx:
npx astrasync register --name invoice-bot --agent-type autonomous \
--model-provider anthropic --model-name claude-sonnet-4-5 \
--framework-name langchain --framework-version 0.3.0 \
--protocols a2a,mcp \
--api-endpoint https://invoice-bot.example.com \
--pdlss '{"purpose":{"categories":["accounting"],"allowedActions":["accounting.read"]}}'The CLI uses the same response logic — it will print a "Sign in to approve" message and a poll URL when the response is 202 pending, and exit non-zero on deny/expire.
/registrationis for one-shot onboarding — register an agent, look up its public profile, check API health. For per-request credential injection (attaching ASTRA-id, sessionId, PDLSS to outgoing HTTP / A2A / MCP calls from an already-registered agent), use/agent(AgentClient). The two roles are deliberately separate concerns: registration is identity,/agentis runtime.
A
PDLSSConfigtype exists in both/registrationand/agent, with different shapes./registration'sPDLSSConfigis the boundary declaration submitted at register time;/agent'sPDLSSConfigis the per-request runtime request shape. Disambiguate via the import path — the root export deliberately does not re-export either.
Inner-hop verification (cross-hop dedupe with X-Astra-Verified-Hop)
When an upstream AstraSync hop and an inner gateway-protected endpoint
both verify the same agent in the same logical request, two verify-access
calls fire — audit gets noisy and you pay a doubled round-trip. The
X-Astra-Verified-Hop header marks the inner hop as already-verified by
the upstream so the inner middleware skips the redundant call.
Two upstreams emit the marker automatically (SDK 4.3.0+):
- the MCP middleware, on its outbound response (tool handlers calling inner REST hops forward it on outgoing fetches — worked example below);
- the edge gateway (
adapter-lambda/adapter-edge), toward the origin whenever its verify-access call produced identity (authenticate / authorize depth). An SDK-middleware origin behind the TAG then skips the double verify by settingtrustVerifiedHop: true— at the default classify depth no marker is emitted and the origin verifies as usual.
trustVerifiedHop + verifiedHopMaxAgeMs are supported on the express,
nextjs, and mcp middleware options (all default off). The inner
middleware validates the marker via parseVerifiedHop +
isVerifiedHopValidFor and skips a duplicate verify-access only if the
marker is fresh AND matches the agent claimed on the inner hop. The edge
gateway strips any inbound X-Astra-Verified-Hop on evaluated passes, so
a viewer can never smuggle one through the TAG.
Trust model (read before enabling): the marker is UNSIGNED — a dedupe
advisory, not proof. Enable trustVerifiedHop only when every network
path to the inner hop crosses a stripping gateway (the TAG) or an
equivalent trusted boundary; inside that boundary a forger could reduce
verify-access from "every request" to "once per freshness window" for an
agent id it already presents. This is the same trust model as the
unsigned X-AstraSync-* attestation headers.
Worked example — upstream MCP → inner REST hop:
// === outer MCP server ===
import express from 'express';
import { createMcpMiddleware } from '@astrasyncai/verification-gateway/mcp';
const mcp = express();
// Behind a load balancer / CDN, set trust proxy to your real hop count so
// req.ip (the client IP recorded on verify-access) is the caller, not the
// proxy. The middleware never trusts a raw leftmost X-Forwarded-For over it.
mcp.set('trust proxy', 1);
mcp.use(express.json());
mcp.use(
createMcpMiddleware({
apiBaseUrl: 'https://astrasync.ai/api',
apiKey: process.env.ASTRASYNC_API_KEY,
counterpartyId: 'ASTRAE-mcp...',
toolGates: { start_checkout: 'standard' },
})
);
// MCP middleware sets X-Astra-Verified-Hop on the response automatically
// AND populates req.agentVerification. Tool handlers can read both.
mcp.post('/mcp', async (req, res) => {
const v = req.agentVerification!;
// Forward the verified-hop marker on outgoing inner-hop calls.
// Build it from the same fields the middleware uses on the response.
const { serializeVerifiedHop, MCP_VERIFIED_HOP_HEADER } =
await import('@astrasyncai/verification-gateway/mcp');
const hop = serializeVerifiedHop({
astraId: v.agent!.astraId,
sessionId: v.sessionId,
checkedAt: Date.now(),
});
const inner = await fetch('http://internal/api/checkout/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Astra-Id': v.agent!.astraId, // identity marker
[MCP_VERIFIED_HOP_HEADER]: hop, // verified-hop dedupe marker
},
body: JSON.stringify({ sku: 'sku_42' }),
});
res.status(inner.status).json(await inner.json());
});
// === inner REST hop (same fleet) ===
import { createMiddleware } from '@astrasyncai/verification-gateway/express';
const rest = express();
rest.use(
createMiddleware({
apiBaseUrl: 'https://astrasync.ai/api',
apiKey: process.env.ASTRASYNC_API_KEY,
counterpartyId: 'ASTRAE-rest...',
trustVerifiedHop: true, // accept the dedupe marker
verifiedHopMaxAgeMs: 60_000, // 60s default; tighten for high-stakes
})
);
// Inner middleware reads X-Astra-Verified-Hop, validates via
// parseVerifiedHop + isVerifiedHopValidFor, and skips verify-access
// when the marker is fresh AND its astraId matches the X-Astra-Id on
// this request.Important: the marker is NOT proof of identity by itself — pair it
with X-Astra-Id so the inner middleware can verify the marker matches
the claimed agent. The marker only gates the dedupe-skip decision.
Access Decision (5.0.0 — two axes, no bands)
The graded accessLevel band was removed in 5.0.0. A verification is now two
explicit booleans:
| Axis | Question it answers |
| ------------------ | -------------------------------------------------------- |
| identityVerified | WHO — did the caller resolve to a registered agent? |
| policyAllowed | WHAT — does this request fit the agent's declared PDLSS? |
Both true → proceed. Either false → failures[] says exactly which dimension
failed and what fixes it. Value-gating (autonomous limit / hard limit) rides
recommendation + stepUpApproval, not an access band.
Trust Levels
| Level | Score Range | | -------- | ----------- | | BRONZE | 0-39 | | SILVER | 40-59 | | GOLD | 60-79 | | PLATINUM | 80-100 |
UI Components
The package includes React components for displaying verification status:
import {
VerificationInterstitial,
TrustLevelBadge,
GuidanceCard,
} from '@astrasyncai/verification-gateway/ui';
// Verification interstitial overlay (renamed from CommerceShield in 4.1.0;
// the old component/hook/props names still work as deprecated aliases)
<VerificationInterstitial
visible={!verified}
result={verificationResult}
onRegister={() => window.location.href = '/register'}
allowGuestAccess={true}
/>
// Trust level badge
<TrustLevelBadge level="GOLD" score={75} />
// Guidance card
<GuidanceCard guidance={verificationResult.guidance} />Credential Extraction
Agents can provide credentials via:
Headers (recommended):
X-Astra-Id: Agent ASTRA-IDX-Api-Key: API keyAuthorization: Bearer <jwt>: JWT token
Query Parameters (fallback):
?astraId=ASTRA-xxx?apiKey=xxx
Verification Response
interface VerificationResult {
// 5.0.0: two explicit axes replaced the old verified/accessLevel band.
identityVerified: boolean; // WHO: the caller resolved to a registered agent
policyAllowed: boolean; // WHAT: the request fits the agent's PDLSS boundary
agent?: {
astraId: string;
name: string;
trustScore: number;
trustLevel: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM';
blockchainAnchored: boolean;
};
developer?: {
astradId: string;
verified: boolean;
};
organization?: {
name: string;
verified: boolean;
trustScore: number;
};
pdlss?: {
purposeAllowed: boolean;
withinDuration: boolean;
withinLimits: boolean;
scopeAllowed: boolean;
selfInstantiationAllowed: boolean;
};
guidance?: {
message: string;
registrationUrl: string;
documentationUrl: string;
steps?: string[];
};
denialReasons?: string[];
}Settlement Authorization
For direct-path merchants settling a priced cart, call authorizeSettlement() after pricing — the middleware only verifies identity/access, not the transaction value:
import { authorizeSettlement } from '@astrasyncai/verification-gateway';
const decision = await authorizeSettlement(config, {
agentId: req.agentVerification.agent.astraId,
value: cart.total, // YOUR authoritative priced total, never agent-supplied
currency: 'USD',
});
if (!decision.authorized) {
// decision.stepUpApproval?.pollUrl — if in the approval band, the owner can approve
return res.status(402).json({ error: decision.reason, stepUpApproval: decision.stepUpApproval });
}
// Safe to settleStep-Up Approval
When a transaction value is between the agent's Autonomous Limit and Hard Limit, verify-access returns stepUpApproval on the result:
interface StepUpApprovalInfo {
approvalId: string; // Capability token (UUID)
pollUrl: string; // GET /api/step-up-approvals/poll/:approvalId
expiresAt: string; // ISO-8601 (decision window: 30 min, aligned to the intent mandate)
}Poll the pollUrl (unauthenticated, rate-limited 60 req/min) to check if the owner approved. The getApprovalPollingInfo(result) helper extracts it from a VerificationResult.
Attempt chain (X-Astra-Attempt-Id, 5.8.0)
Every interaction chain carries an attempt id of the form att_<32 hex>. The
AstraSync bridge stamps it on each call it delivers to your endpoint as the
X-Astra-Attempt-Id request header, and the express / Next.js middleware
automatically read the header and thread the id into verify-access (invalid
values are dropped). Two effects:
- One intent = one approval. A step-up the owner already approved for this chain passes every later verify-access hop that carries the same attempt id (the bridge pre-verify, your middleware's re-verify, and any re-drive after a delivery failure) — it is never asked for twice.
- Idempotency key. Key your own idempotency/dedupe on the header value: a re-drive of the same chain arrives with the SAME id, so replays of an already-executed action can be detected venue-side.
No configuration is needed; direct (non-bridge) callers simply don't send the header and behaviour is unchanged.
Bridge-fronted endpoints: hold budgets
The bridge delivers your endpoint's response within an HTTP budget of 30 s
by default (60 s maximum, configurable per route via responseBudgetMs in
your registration's routes[]). stepUpMaxWaitMs: 0 (hold in-request
until the approval decides) is designed for direct integrations and will
outlive that budget — behind the bridge, either:
- leave
stepUpMaxWaitMsunset and return the 403 withstepUpApprovalin-band (the bridge renders the approval card and re-drives after approval), or - bound the hold to well under the budget (~20 s) so the deny still reaches the bridge in time.
Either way the attempt chain above guarantees the post-approval re-drive passes without a second approval.
Checkout & settlement (astra-pay)
The first-party commerce rail: an agent shops under its ASTRA-id, holds NO payment credential, and the platform settles from the owner's saved instrument. Two phases discriminate quote from money movement:
commercePhase: 'quote'(or unphased) — verification only, never settles.commercePhase: 'confirm'— the ONLY leg that can settle. Useclient.confirmCheckout({ astraId, transactionValue, currency, checkoutSessionId, checkoutItems, counterpartyUrl });checkoutSessionIdis the per-cart idempotency key (one order row per session, normative).
Read result.settlementOutcome.status — never success alone:
| status | Meaning | Your action |
| ------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| pending_merchant | 5.14.1: order recorded — YOU settle it | Charge the settlementToken (card) or redeem the settlement voucher (stablecoin), then reportSettlement() |
| settled | Money moved (reported / webhook) | Fulfil / mark paid |
| requires_approval | Held for human step-up — not a failure | Record as pending; a re-drive follows approval |
| failed | Charge declined (failureCode) | Mark failed |
| requires_action | 3DS/SCA needed on the saved card | Per your policy |
| no_instrument | Policy passed, no chargeable card on file | Terminal on autonomous; NON-terminal after approval — the human's approval survives; re-drive the same session once a card exists |
| anything else | A status newer than your SDK | Treat as PENDING, never failed (open union, 5.5.0) |
Every merchant settles (5.14.1 — AstraSync never charges): a purchase
grant records the order and returns settlementOutcome: { status:
'pending_merchant', orderId } with what you settle:
- Card, first-party storefront (our Stripe tenant):
settlementToken(agent-blind{ stripeCustomerId, stripePaymentMethodId, amountMinor, currency, sessionId, orderId, jti, statementSuffix, expiresAt }). Charge on your own integration with Stripe idempotency keyvoucher:<token.jti>andmetadata: { jti, sessionId }, thenclient.reportSettlement({ orderId, status, amountMinor, currency, processorRef }). - Stablecoin, any merchant: the
settlementvoucher. Redeem it (the order moves toredeemed), move the funds, thenclient.reportSettlement({ orderId, status: 'settled', amountMinor, currency, txHash, chainId }). - The request's protocol carried the payment (ACP / MPP token, x402 payload, AP2 payment mandate): nothing is minted; settle with that credential and report back.
Amounts must match the order exactly; replays are safe. An order nobody settles expires 10 minutes after its artifact does; a late report still settles it. Never expose the token or voucher to the agent plane or logs.
Fulfilment PII (5.6.0): first-party confirm results carry a
fulfilment: { email, emailSource } block — you ALWAYS get a fulfilment
email for a first-party order. emailSource: 'agent_provided' means the
agent passed a buyerEmail (on the bridge handoff body, the verify-access
request, or your confirmCheckout({ buyerEmail }) call);
emailSource: 'account' means the platform defaulted to the buyer's account
email. Emails arrive canonicalized (trimmed, lower-cased). Precedence: an
explicit handoff-body buyerEmail wins over fulfilment.email's account
default. Store the email with the pending order and fulfil against it only
once the order reaches settled / pending_merchant. All of it is
transit-only — AstraSync stores nothing; treat fulfilment as merchant-only
material (the bridge strips it from the agent plane). Shipping addresses
never transit AstraSync: take them agent → your fulfilment endpoint
directly, joined by sessionId.
Settlement Artifacts
On a clean merchant-mediated grant where the owner has a verified payment instrument, verify-access returns a settlement object (wire format v2, versioned via the ver: 2 claim — verify/redeem reject other versions):
interface SettlementArtifact {
type: string; // e.g. "stablecoin_voucher"
artifact: string; // JWS compact-serialised (ES256) — the authoritative, signed object
binding: FiatSettlementBinding | StablecoinSettlementBinding; // display-only mirror
}
// Stablecoin voucher (v2): integer minor units, pinned settlement asset
interface StablecoinSettlementBinding {
merchantId: string;
amountMinor: number; // integer minor units — no floats anywhere
assetDecimals: number; // scale for display: amountMinor / 10^assetDecimals
currency: string; // e.g. "USDC"
chainId: number; // the chain this voucher settles on
tokenContract: string; // the token contract it settles in
asset: string; // CAIP-19 asset id
sessionId: string;
singleUse: true;
expiresAt: string;
}
// Fiat/processor artifacts (Stripe, Skyfire, PayPal): the processor is the
// unit authority, so the binding carries the major-unit pricing amount
interface FiatSettlementBinding {
merchantId: string;
amount: number;
currency: string;
sessionId: string;
singleUse: true;
expiresAt: string;
}binding is display-only convenience. It mirrors the signed JWT claims but is not itself signed — render it, never settle from it. Decide from the server-verified payload returned by redeem.
Voucher lifecycle (server-to-server)
| Endpoint | Purpose |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| POST /api/wallets/voucher/verify | Verify signature/issuer and return the decoded payload — proves authenticity only, no spend |
| POST /api/wallets/voucher/redeem | Atomic single-use spend: first caller gets 200 redeemed (plus order: { status: 'redeemed' } when the voucher funds a recorded order — 5.14.1, no charge happens here), replays get 409 already_redeemed (with first-redeemer forensics), revoked vouchers get 409 revoked |
| GET /api/wallets/voucher/.well-known/jwks.json | Public JWKS for offline signature verification |
Redeem accepts optional expectedAmountMinor / expectedCurrency — mismatches are rejected (400 amount_mismatch / currency_mismatch) before the voucher is spent, so a mismatch never burns the voucher. Vouchers carry aud = your merchant id; declare merchantId on verify/redeem to have the audience check enforced (standard JWT libraries give you the same check free on offline verification).
Single-use is enforced server-side by an atomic claim on the voucher id — N of your nodes presenting the same voucher get exactly one 200 and N−1 409s, with no coordination between them.
What a successful redeem proves — and what it doesn't
A 200 from redeem proves, at that moment:
- the voucher is authentic — signed by AstraSync, unexpired, format-valid;
- the agent was authorised to commit this spend when the voucher was minted — it passed verification and the amount was within the owner's configured limits;
- the voucher is bound to you (audience = your merchant id) and to the stated amount, currency, and settlement asset;
- the spend is exactly-once — this voucher was never redeemed before, was not revoked, and can never be redeemed again.
It does not verify that funds exist in, or are reserved from, the owner's wallet. The voucher is a payment authorization, not a funds guarantee — today, funds assurance at settlement time comes from your own settlement rail, exactly as it does for any other payer.
Coming later: a settlement upgrade that makes redemption and funds transfer a single atomic on-chain event. A voucher will only redeem if the funds actually move — if the wallet can't cover it, the redemption fails and the voucher is not consumed — making "redeemed" and "funded" the same event. The wire format already carries what that needs (pinned chain, token contract, integer minor units), so no integration change is expected beyond opting in.
Configuration
interface GatewayConfig {
// Required. Always include the /api path prefix — for prod use
// 'https://astrasync.ai/api', for staging 'https://staging.astrasync.ai/api'.
apiBaseUrl: string;
// Optional
apiKey?: string; // For authenticated requests
cacheTtl?: number; // Cache duration in seconds (default: 300)
debug?: boolean; // Enable debug logging
// Counterparty attribution (v2.2.3+)
counterpartyUrl?: string; // Sent with verify-access for analytics
counterpartyType?: 'agent' | 'api' | 'mcp_server' | 'website' | 'other' | 'unknown';
counterpartyId?: string; // Your ASTRAE-id (issued at endpoint registration);
// forwarded on every verify-access call so the server attributes traffic
// directly to this endpoint rather than resolving by URL.
// Step-up hold-and-poll (v3.6.0+, OPT-IN). When stepUpMaxWaitMs is set, the
// express/mcp/nextjs middleware HOLDS a step_up_required request, polls the
// approval, and on 'approved' re-verifies once (cache-bypassed) so the
// backend redeems the single-use approval — the owner approves in their
// dashboard while the original request waits (MFA model). Unset = current
// fail-closed behavior (immediate 403 with stepUpApproval polling info).
// 0 → hold until the approval's expiresAt (+5s grace; 5-min fallback)
// >0 → hold at most this many milliseconds
// CAUTION: Next.js edge runtimes enforce ~25s wall-clock budgets — use a
// small value there (≤20000) or gate in a Node route handler instead.
stepUpMaxWaitMs?: number;
// Poll interval for the hold (default 3000ms, 250ms floor). The poll route
// is rate-limited 60/min/IP: at the default interval ~3 concurrent holds
// per merchant IP fit; excess polls degrade the hold to 'timeout'.
stepUpPollIntervalMs?: number;
// Init self-test (v2.2.3+) — fires a HEAD probe to verify-access on first
// call and warns if apiBaseUrl is pointing at HTML (catches the marketing-
// 404 case). Set true for tests where the extra request is undesirable.
disableInitChecks?: boolean;
// @deprecated — removed as functional config in v2.3.0; the accessLevel
// band itself was removed in 5.0.0 (identityVerified + policyAllowed are
// the decision axes). Setting these has no effect (a one-shot
// console.warn fires). To gate access to your endpoint, configure
// trust_score_requirement server-side via the /api/endpoints registration.
minTrustScore?: number;
minTrustScoreForFull?: number;
}Verification interstitial
When an unverified agent visits a protected web page, the Next.js middleware serves the verification interstitial — an HTML overlay (user-visible title: "AstraSync Agent Verification") that displays:
- Registration guidance
- Steps to get verified
- Link to documentation
- Optional guest access
This creates a smooth experience for agents while maintaining security. Control it with showInterstitial (default true) on the Next.js middleware options; the React building blocks ship on the ./ui subpath (see UI Components).
Renamed in 4.1.0 — this feature was previously called "Commerce Shield". The old names still work as deprecated aliases and will be removed in the next major:
showCommerceShield(option;showInterstitialwins when both are set),CommerceShield/useCommerceShield/CommerceShieldProps(from./uiand the root barrel).
Trusted Agent Gateway (edge adapters)
The Trusted Agent Gateway (TAG) is AstraSync's verification gateway at the CDN edge: it classifies and (optionally) verifies every inbound agent request in front of your existing site — in observe mode (verify, classify, and record everything; block nothing) or enforce mode — with zero site changes. Two packages deploy it:
@astrasyncai/adapter-lambda— CloudFront / Lambda@Edge (CloudFormation template + deploy script included).@astrasyncai/adapter-edge— Vercel Edge Middleware, Cloudflare Workers, and Fastly Compute, via per-platform subpaths (shipping in this release).
Both consume the same dashboard-managed policy (./edge-config), the same platform fingerprint registry (./platform-signatures), and the same header sanitiser (./metadata-capture) as this SDK — one classification everywhere.
What each integration captures (canonical capture matrix)
Header-level capture (ObservedMetadata.headers etc.) is identical everywhere: automatic on every verify for the edge adapters and the express / nextjs / mcp SDK adapters (no config, no opt-out). What differs is the connection layer — and the deciding variable is who operates the CDN in front of your origin:
| Integration | Connection layer | TLS fingerprint (JA3/JA4) |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Edge adapter (adapter-lambda, adapter-edge) | Full — native platform signals (IP, ASN, geo, TLS, HTTP version, device class, header structure) | CloudFront: JA3 + JA4 (2.2.0+, via the template's origin request policy). Cloudflare Workers: Bot Management zones only. Vercel: JA4 with BotID. Fastly: none |
| SDK behind a CDN you control (your own Cloudflare / Fastly / Vercel / CloudFront distribution) | Derived — geo/ASN/TLS promoted from the CDN's injected headers (deriveConnectionFromHeaders) | Cloudflare: JA3 + JA4 (cf-ja3-hash / cf-ja4, plan-dependent). CloudFront: JA3 + JA4 — add the CloudFront-Viewer-JA3/JA4-Fingerprint headers to your origin request policy |
| SDK behind a platform-managed CDN (Railway, Render, Fly, Heroku, …) | IP only — platform CDNs forward x-forwarded-for but NOT their provider-specific geo/TLS headers to customer origins | None — no fingerprint header reaches your origin |
| SDK, no CDN | Absent (IP from x-forwarded-for when a proxy sets it) | None |
"Behind Fastly" ≠ "receives Fastly's headers": platform-managed CDNs are operated by the platform, and they do not forward
Fastly-Client-IP-style headers to your origin. If connection-layer signals or fingerprints matter to you on a managed platform, deploy the edge adapter in front (bucket 1) — that is the whole point of the TAG.
Note:
runCommercePipeline(on the./transportsubpath) is the server-side orchestrator of commerce-protocol verification — it runs inside the AstraSync verify-access service. The edge adapters forward raw commerce artifacts to verify-access and never bundle or call the pipeline themselves.
Which EdgeConfig fields each integration consumes
The dashboard-managed EdgeConfig drives the edge posture. The observe/enforce × depth posture is edge-only by design: SDK adapters enforce via your dashboard route policy (fetched automatically on startup), not via EdgeConfig — the SDK consumes only the sampling block, which governs its anonymous traffic beacon.
| EdgeConfig field | Edge adapters (adapter-lambda, adapter-edge) | SDK adapters (express / nextjs / mcp) |
| ------------------------------ | ------------------------------------------------ | ------------------------------------------------- |
| mode (observe / enforce) | Yes — the deployment posture | — (enforcement comes from dashboard route policy) |
| depth | Yes — classify / authenticate / authorize | — |
| pathRules | Yes — per-path mode/depth/skip overrides | — |
| sampling.anonymousBeaconRate | Yes — anonymous-bot beacon sampling | Yes — governs the SDK anonymous beacon |
| failurePosture | Yes — always fail-open in v1 | — |
Metadata capture
Three subpath exports back the capture machinery (all edge-safe — no Node built-ins — and also re-exported flat from the package root):
./metadata-capture
The ONE sanitiser deciding what is safe to store from an inbound agent's request. Used identically by the edge adapters, the SDK adapters, and the backend.
sanitizeHeaders(raw)→{ headers, apiKeyFormat?, platformHeaders? }— drops genuine secrets (cookies, signatures, AstraSync credential headers), reduces credential headers to a safe format prefix (e.g.sk-ant-api03— a platform signal, never the key), keeps everything else verbatim, and enforces size caps (MAX_HEADERS,MAX_HEADER_VALUE_BYTES,MAX_TOTAL_BYTES).extractApiKeyFormat(value)— the safe key-format prefix of a credential header value, orundefinedfor unrecognised/opaque tokens.extractPlatformHeaders(sanitized)— the platform-signal subset (openai-organization,anthropic-version,x-goog-user-project, …) of an already-sanitised map.deriveConnectionFromHeaders(headers)— promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront geo, client-IP, TLS-fingerprint, and header-structure headers — incl.cf-ja3-hash/cf-ja4andcloudfront-viewer-ja3-fingerprint/-ja4-fingerprintfrom 4.3.0) into theconnectionblock, so SDK-only deployments behind a CDN you control still get connection-layer attribution (see the capture matrix for the platform-managed-CDN caveat). Values are truncated toMAX_CONNECTION_VALUE_CHARS(256 — the platform bound). Native edge signals remain authoritative; this is the SDK-level fallback.buildSdkObservedMetadata(rawHeaders)— the one builder the express / nextjs / mcp adapters share: sanitised headers +apiKeyFormat+platformHeaders+ derivedconnection+schemaVersion.CAPTURE_SCHEMA_VERSION— the capture-semantics version stamped on everyObservedMetadata(currently1). Capture semantics are frozen within a major version:schemaVersionbumps only on a semantic change to what is kept/dropped/derived, never for additive signals. The verify body'ssdkVersiondisambiguates builds.- Types:
ObservedMetadata({ headers, apiKeyFormat?, platformHeaders?, connection?, schemaVersion? }—connectionis populated natively by edge adapters and via CDN-header derivation by SDK adapters),SanitizeHeadersResult.
./platform-signatures
The shared registry of known platform-agent fingerprints (Claude / ChatGPT / Gemini / Cursor / Goose / Perplexity) — the single source of truth for backend and edge classification.
PLATFORM_AGENT_SIGNATURES— the seed signature set (UA patterns, agent-card patterns, API-key-format patterns, header patterns).detectPlatformFingerprint(input)→{ vendor, fingerprintKey, displayName } | undefined— first-match-wins detection over the seed set.matchPlatformSignature(signatures, input)— the same matcher over a caller-supplied signature set (for merged/dynamic registries).
./edge-config
The per-endpoint policy a Trusted Agent Gateway deployment fetches from the AstraSync dashboard.
getEdgeConfig(config, counterpartyId)— cached fetch (TTL + stale-while-revalidate). Fail posture is asymmetric by design: an unreachable backend can never turn enforcement ON, and a config stale for >24h automatically degrades anyenforceback toobserve.fetchEdgeConfig(...)— one uncached fetch (ETag/304 aware);matchEdgePathRule(rules, path)— first-match-wins*-glob path rules;degradeToObserve(config);isEdgeConfig(value);DEFAULT_EDGE_CONFIG(observe-only, classify depth).- Types:
EdgeConfig({ version, mode, depth, pathRules, sampling, failurePosture }),EdgeMode('observe' | 'enforce'),EdgeVerificationDepth('classify' | 'authenticate' | 'authorize'),EdgePathRule.
Two baseUrl conventions — same package, different clients
This package ships two clients. They use slightly different URL conventions; this is intentional but worth pinning down:
| Client | Field | Expected value | Example |
| ---------------------------------------------------------------------------- | -------------------------- | ----------------------- | -------------------------- |
| Verification gateway (createMiddleware, createMcpMiddleware, verify()) | GatewayConfig.apiBaseUrl | Origin + /api | https://astrasync.ai/api |
| Registration SDK (new AstraSync(...)) | AstraSyncConfig.baseUrl | Bare origin (no /api) | https://astrasync.ai |
Why the split: the gateway derives multiple endpoints from apiBaseUrl (verify-access, fetchRoutes, recordDecision, etc.), all under /api/*. The registration SDK prepends /api/agents/... per call and would double the path if baseUrl already ended with /api.
As of v2.4.2, the registration SDK tolerates a trailing /api on baseUrl — it strips the suffix and emits a one-time console warning so you can fix the source. Pass silent: true in AstraSyncConfig to suppress the warning (e.g. in tests).
Staging vs production
Both environments share the same URL shape (origin + /api for the verify gateway; bare origin for the registration SDK). Promotion is a single hostname swap:
| Environment | Verify gateway (apiBaseUrl) | Registration SDK (baseUrl) |
| ----------- | ---------------------------------- | ------------------------------ |
| Staging | https://staging.astrasync.ai/api | https://staging.astrasync.ai |
| Production | https://astrasync.ai/api | https://astrasync.ai |
Path shape, request bodies, response schemas, error envelopes — all identical. Swap staging.astrasync.ai ↔ astrasync.ai, nothing else.
Header semantics — X-Astra-Gateway-Mode
The SDK sets X-Astra-Gateway-Mode on every response that's been through the gateway middleware. The value describes the GATE state — not whether the request succeeded end-to-end.
| Value | Meaning |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| enforced | The gateway evaluated policy on this request. Set on denial responses (defaultOnDenied, defaultMcpDenied). |
| unenforced | The gateway skipped policy evaluation for this request. The X-Astra-Gateway-Reason header explains why (mcp-tier-none, non-jsonrpc-body, no-policy, no-match, route-none). The downstream handler may still return any status. |
Pre-v2.4.2 used the value pass-through — renamed in v2.4.2 to disambiguate "gate skipped" from "request allowed".
Local Guard for Claude Code
Native governance for Claude Code via its PreToolUse hook (subpath ./claude-code, shipped in v3.7.0). Every tool call — file reads/writes, shell commands, web fetches, and optionally MCP tools — is evaluated against a local PDLSS policy before it runs. No proxy, no cloud dependency.
npm install -g @astrasyncai/verification-gateway
# User-level install (~/.claude + ~/.astrasync); add --project for repo-scoped
astrasync-guard install-claude-codeThe installer registers the astrasync-claude-hook binary in Claude Code's settings.json and creates .astrasync/policy.yaml (the PDLSS policy — re-read on every tool call) plus .astrasync/guard.json (guard settings). Flags: --project (install into the current repo; a project policy overrides the user-level one), --wizard (interactive policy builder), --active (start enforcing immediately), --policy <path> (use an existing policy file).
- Posture — installs default to
"posture": "passive": decisions are evaluated and written to JSONL traces under.astrasync/traces/, nothing is blocked. Flip to"active"inguard.jsonto enforce: DENY blocks the tool call with a reason (permissionDecision: "deny"), MANUAL_REVIEW routes to Claude Code's own permission prompt ("ask"), and ALLOW emits nothing — the guard only tightens, it can never bypass Claude Code's native permission system. - Safe by default — a machine with no policy file is a silent no-op; a corrupt
guard.jsonfalls back to enforcing defaults rather than silently loosening the guard.ASTRASYNC_GUARD_DISABLE=1turns the hook off;ASTRASYNC_GUARD_FAIL_OPEN=1allows on internal hook errors (default is fail-closed in active posture). - MCP governance — set
"governMcp": trueto govern MCP tools too;mcp__{server}__{tool}maps to purpose{server}.{tool}in the policy. - Uninstall —
astrasync-guard uninstall-claude-coderemoves the hook; policy and traces are left in place.
Changelog
v4.1.0 — Trusted Agent Gateway naming + MCP metadata capture
- The Next.js unverified-agent overlay is now the verification interstitial:
showInterstitialoption,VerificationInterstitial/useVerificationInterstitial/VerificationInterstitialProps. The oldshowCommerceShield/CommerceShield/useCommerceShield/CommerceShieldPropsnames keep working as deprecated aliases until the next major (showInterstitialwins when both options are set). - The MCP middleware now captures header-level
ObservedMetadataon every verify-access call (parity with the express/nextjs adapters). - The edge product is the Trusted Agent Gateway (TAG) — see the new README sections above.
v3.8.0 — Settlement voucher wire format v2
- BREAKING type change:
SettlementArtifact.bindingis now a union —StablecoinSettlementBinding(v2:amountMinorinteger minor units +assetDecimals, pinnedchainId/tokenContract/CAIP-19asset) orFiatSettlementBinding(processor artifacts keep major-unitamount). The floatamountfield is gone from stablecoin bindings. Matches the server'sver: 2wire format, which redeems/verifies reject other versions of. - Settlement Artifacts docs rewritten: voucher lifecycle endpoints (verify / redeem / JWKS), expected-value gates on redeem, revocation semantics, and an explicit guarantee boundary — what a successful redeem proves and what it doesn't (redeem proves the authorization is live and single-use; it does not check or move funds).
v5.16.0 — ChallengeHandler auto-include removed
ChallengeHandleranswers only for counterparties registered as pending — the 3.7.1 auto-include let anyone presenting an agent's ASTRA-id pass its runtime challenge. PasschallengeHandlertoAgentClientandfetch()registers each call while it is in flight; otherwise callregisterPending()/removePending()around each interaction.
v3.7.1 — ChallengeHandler auto-include (removed in 5.16.0)
ChallengeHandlerauto-includes the incomingcounterpartyId/counterpartyUrlin the challenge response when no counterparties were pre-registered viaregisterPending()— the correct default for agents that never call it. Pre-registered whitelists behave exactly as before.- Runtime-challenge docs: auto-include default + double-appended-path warning.
v3.7.0 — AstraSync Local Guard for Claude Code
./claude-codesubpath export — Layer 4 Local Guard adapter for Claude Code's PreToolUse hook (see the Local Guard section above). Strictly tightening: DENY →deny, MANUAL_REVIEW →ask, ALLOW → no output.astrasync-claude-hookbinary +astrasync-guard install-claude-code/uninstall-claude-codeinstaller commands.
v3.6.0 — Step-up hold-and-poll
- Opt-in
stepUpMaxWaitMs/stepUpPollIntervalMsonGatewayConfig: middleware holds astep_up_requiredrequest, polls the approval, and onapprovedre-verifies cache-bypassed so the backend redeems the single-use approval. Non-approved outcomes deny withstepUpOutcome(denied | expired | consumed | timeout) and anX-Astra-Step-Upheader. OFF by default. awaitStepUpApproval()/pollStepUpStatus()exported for direct use.reportUnregisteredAttemptsends the configuredcounterpartyId(anon-lead attribution is id-first) and omits a nullcounterpartyUrl.
v3.5.0 — Partner feedback round
extractTransactionValueoption on Express middleware — opt-in upfront value extraction so PDLSS limit evaluation runs at middleware time (authorizeSettlement()remains the required fail-closed settlement gate).- Settlement architecture: merchant-mediated grants are voucher-primary — only crypto wallets mint settlement artifacts; fiat instruments log a diagnostic and do not mint.
v3.4.0 — Type alignment for LMAX settlement + step-up approval
StepUpApprovalInfoandSettlementArtifactinterfaces added toVerificationResultstepUpApprovalsurfaced in Express/MCP/Next.js adapter deny response bodiesSettlementDecisioncarriesstepUpApprovalon step-up denials viaauthorizeSettlement()getApprovalPollingInfo()simplified — typed field, no moreunknowncastsAttestation.checkedAt(required) — merchant freshness gate timestampVERSIONconstant updated to3.4.0
v3.3.0 — Direct-path value enforcement
authorizeSettlement(config, { agentId, value, currency })— fail-closed settlement gate for direct-path merchantsrestrictionssurfacesapprovalThreshold(enforced per-tx) andmaxPerPeriod(not yet enforced)- ASCII-safe agent-facing strings
v3.2.1 — Platform-agent go-live readiness
- Canonical PDLSS limits terminology (
autonomousThreshold,approvalThreshold) - Step-up/approval fail-closed in Express/MCP/Next.js adapters via
approval-gate.ts
v3.2.0 — Commerce observability
- Access-level band no longer gates (informational only);
requiresStepUpcarries the signal - Trust score redacted from agent-facing responses
- Cross-merchant cache key fix
v3.1.0 — Canonical PDLSS vocabulary (Bug 14)
- Two-axis purpose/action chains with dotted action tokens
- Route send-mapping for tool→semantic-action translation
v2.4.6 — Round-14 partner integration testing
⚠️ BREAKING CHANGE — endpointUrl → counterpartyUrl on POST /api/endpoints AND PUT /api/endpoints/{id}
The body field renamed to align with every other surface (verify-access,
dashboard policy, SDK config — all use counterparty*). Applies to BOTH
create (POST) and update (PUT) verbs:
POST /api/endpoints— request body usescounterpartyUrl(wasendpointUrl).PUT /api/endpoints/{id}— same. The strict-mode validator on both verbs returns a clean 400unrecognized_keysnamingcounterpartyUrlas the expected key when partners send the old field name.- Response shape (
GET /api/endpoints/{id}) also renamed for full symmetry — partners receivecounterpartyUrlon read AND sendcounterpartyUrlon write. DB columnendpoint_urlstays as the internal join key; the service layer maps the public name to the DB column.
Migration: if your code posts endpointUrl to POST /api/endpoints
OR PUT /api/endpoints/{id}, rename to counterpartyUrl. If your code
reads .endpointUrl from GET responses, rename to .counterpartyUrl.
No legacy alias is shipped — clean break per the
feedback_hard_break_no_legacy_shim discipline.
Other round-14 items:
Item 1 — F8 runtime-challenge gate moved from SEND to RECOMMENDATION (
agents.routes.ts). Round-12 placed the gate at the SEND (if (data.enableRuntimeChallenge && counterparty.requiresRuntimeChallenge)) which suppressed the challenge entirely on F8=optional, losing the trust-scoring data the result feeds. Round-14 moves the gate to the recommendation outcome:- challenge ALWAYS fires when
enableRuntimeChallenge: true(captures trust-scoring data for every call; the result lands on the response body'sruntimeChallengeblock regardless of F8 setting) - failure / timeout only escalates
recommendation(→deny/step_up_required) when the endpoint declaredrequiresRuntimeChallenge: true
Extracted into a pure helper
deriveRuntimeChallengeRecommendationso the 6-quadrant truth table is unit-testable without the full verify-access stack. See/docs/agent-access/runtime-challengefor the partner-facing contract page (new in this round).- challenge ALWAYS fires when
New
/docs/agent-access/runtime-challengepage documenting the agent-side challenge contract — request shape, response shape, HTTP status semantics, signing posture (unsigned v1; DPoP RFC 9449 on the roadmap as the cryptographic upgrade path), timing, replay semantics. Headlines theChallengeHandlerdrop-in from@astrasyncai/verification-gateway/agent; curl is the wire-spec fallback.New
/docs/mcp-integration"Purpose + action precedence" section documenting the 4-tier chain (header → _meta → arguments → default) that applies to both purpose and action. Closes the round-13 SDK-README- only documentation gap./docs/agent-accessrevisions: section 4b expanded to a dedicated "Headers an integrating agent must send" section coveringX-Astra-Id,X-Astra-Purpose,X-Astra-Action(the last is new partner-facing documentation for the round-13 R13-2 header). Section 5 invertedtokenGuidancetext corrected. New section 5b "What verify-access tells you" annotates the full grant payload with cross-links to the SDK's exported TypeScript types (VerificationResult,TokenGuidance,EnhancedVerificationResult)./docs/merchantsadditions: worked endpoint POST + PUT examples using the newcounterpartyUrlname; per-protocol terminal-status table with explicit external-contract preamble (the literals come from each protocol's published spec, not from AstraSync's choice) + auth/capture two-step callout for agent-pay and TAP; A2A JSON-RPC worked example.F14 Health Check tooltip reframed as descriptive-only metadata ("active probe on roadmap") — the storage + serializer + dashboard UI exist but the active probe pipeline isn't implemented yet (deferred to a focused future round). Sets partner expectations correctly while the pipeline lands.
v2.4.5 — Round-13 partner integration testing
⚠️ BREAKING CHANGE — pdlss_immutable → agent_immutable
The 409 response from PUT /api/agents/:id (post-mint mutation attempt)
now returns error: 'agent_immutable' instead of error: 'pdlss_immutable'.
The scope also widened: round-12 rejected only the subset
{ pdlss, model, framework, agentType, apiEndpoint }; round-13 rejects
ANY field except agentStatus (the only allowed lifecycle transition
post-mint).
Migration: if your code catches pdlss_immutable, update to
agent_immutable. No legacy alias is shipped — clean break prevents
permanent shim cruft. The new shape:
{
"success": false,
"error": "agent_immutable",
"message": "Agents are immutable post-approval. ... Attempted immutable fields: <list>. ...",
"immutableFields": ["name", "description", ...]
}Why: agents become immutable at approval + mint per the trust-chain
invariant. Pre-mint owner edits flow through
POST /agents/request-registration/:requestId/approve (dashboard-only,
accepts a full edit body). Post-mint, the only allowed transition is
agentStatus (e.g. dashboard retire button). For configuration changes,
use the upgrade flow (Agent dashboard -> Upgrade, or
POST /agents/:id/upgrade from a dashboard session) — it mints a successor
version with a new ASTRA-id and retires the current one.
Other round-13 items:
R13-1 + R13-2 — MCP middleware: symmetric precedence chain for
purposeandaction. Canonical resolution (documented ONCE, applies to both):X-Astra-<concept>HTTP headerparams._meta.astrasync.<concept>body fieldparams.arguments.<concept>body field- Transport-layer default:
purpose→'mcp_invoke'action→'<method>:<toolName>'(or'<method>'alone)
Round-12 F19 shipped purpose with
header → _meta → default; this round closes theparams.arguments.purposefallback gap AND ships action with the same full chain in one round (not staggered) to pre-empt the parallel "I set action in arguments and it didn't take" support tickets. Resource string staysmcp:tool/<name>regardless.mcpToPdlss(parsed, headerPurpose, headerAction)signature.McpPdlssMapping.purposeSourcenow'header' | 'meta' | 'tool_argument' | 'default_mcp_invoke'(round-12 narrower'header' | 'tool_argument' | 'default_mcp_invoke'widened to splitmetafromtool_argument). New companionactionSource: 'header' | 'meta' | 'tool_argument' | 'transport_layer'.R13-5 — MCP
evaluateAlwaysIfCredentialedparity with F9. Flag moved fromExpressMiddlewareOptionstoGatewayConfigso both adapters inherit. MCP middleware now mirrors the express F9 pattern: route-none + flag-on + credentialed → run verify-access for the audit trail, populatereq.agentVerification, then proceed without gates (X-Astra-Gateway-Mode: enforced,Reason: evaluated-not-enforced). Closes the round-12 deferral.F14 closure —
sdkVersionbody field on verify-access. Replaces round-12's User-Agent regex extraction which silently failed because Node's undici fetch doesn't ship a usable User-Agent header. The SDK now setsbody.sdkVersion = SDK_VERSION(sourced frompackages/verification-gateway/src/version.ts, bumped alongsidepackage.jsonon every release). Backend reads from the body field and runs the same forward-only auto-pop intokya_counterparty.sdk_version. Works in Node, browser, and behind CDNs uniformly.R13-4 — Branded TypeScript types (compile-time protection against the recurring UUID / public-id string-confusion bug class — round-7 #46, round-11 F1, round-12 F15). New
CounterpartyUuid,AgentUuid,OwnerUuid,CounterpartyAstraeId,AgentAstraId,OwnerAstradIdbranded types in the backend atapps/backend/src/types/branded-ids.ts. Zero runtime cost; affects only compile-time assignment comp
