Start here

Merchants — start here

You operate an API, MCP server, or website that AI agents call. You want AstraSync to verify those agents on your behalf, surface a structured policy decision your handlers can act on, and audit-log everything to a blockchain. This page is the fastest path to first-success — clone, configure, ship.

Pick the right npm package

AstraSync ships two packages. As a merchant you only need one of them.

  • @astrasyncai/verification-gateway — install this. Express + Next.js middleware, MCP / A2A adapters, webhook signature verifier. Sits in front of your service and answers verify-access calls on the inbound traffic.
  • @astrasyncai/verification-gateway — that's the agent-author side. Don't install it for the merchant flow.

Mnemonic: verification-gateway = answers, sdk = asks.

Choose your orchestration path

AstraSync supports two integration paths. Pick the one that matches the control vs effort trade-off you want.

Path A

Merchant-orchestrated

You run your own MCP server (or REST / A2A endpoint). Your handler decides what agents can do. AstraSync verifies the agent on your behalf and you act on the structured decision.

Path B

Bridge-orchestrated

You register an endpoint with AstraSync; agents reach you via the AstraSync MCP bridge at mcp.astrasync.ai. No MCP code to write — the bridge exposes discover_catalog, start_checkout, confirm_purchase on your behalf.

Most merchants start with Path B (faster time-to-market) and graduate to Path A when they need richer per-agent UX. Both paths use the same @astrasyncai/verification-gateway middleware for inbound traffic — the difference is whether the MCP surface that agents call lives on your origin (Path A) or on mcp.astrasync.ai (Path B).

Path B: catalog price format

When the AstraSync bridge pulls your catalog (via discover_catalog), it normalises each SKU's price into the authoritative amount used to price checkouts. This is an integrity path — the bridge prices from yourcatalog, never the agent's claim — so emit the canonical shape below for zero ambiguity.

Canonical (recommended) — emit all three:

{
  "id": "sku-001",
  "title": "Widget",
  "price": { "amount": "7.00", "amount_cents": 700, "currency": "USD" },
  "availability": "in_stock"
}

// Zero-decimal currency (JPY) — no subunit:
{ "price": { "amount": "700", "amount_cents": 700, "currency": "JPY" } }

amount = decimal string in major units; the integer field (amount_cents or Stripe unit_amount) = minor units; currency = ISO 4217.

Also accepted (safety net):

  • object aliases: value (≡ amount), price_cents / unit_amount (≡ amount_cents)
  • a scalar price — "7.00" or 7 — read as major unitswith the SKU's top-level currency (a bare scalar cannot signal cents; use amount_cents for minor units)
  • top-level amount_cents/unit_amount when there is no price object

Fail-closed: if a SKU's price can't be parsed into an authoritative amount, that SKU's checkout is refused with sku_price_unparseable (distinct from sku_not_found) — the bridge never falls back to a caller-supplied price. The accepted shapes are a compatibility net; emit the canonical shape so pricing is never ambiguous.

`price` must be the effectiveprice for the caller's tier

The bridge prices the intentMandate andfeeds the PDLSS limit engine (the agent's spend budget) from the catalog's top-level price. So price must be what the caller actually pays — already net of any tier/promo discount — not a list/RRP price with the real price parked in a separate field. If you emit list price, verified buyers are over-charged in the mandate and wrongly budget-denied against an inflated figure. One SKU price = the effective price for the requesting tier.

Which tier? Every catalog access is a verification event

The bridge verifies the caller before fetching your catalog, so every pricing access shows up in your activity feed as a verification event. An anonymous caller is served the anonymous tier (your list/RRP view) and you see an anonymous verification event; an ASTRA-id'd, verifiedcaller is served the verified tier (so you can return promotional/tiered pricing) and you see an attributed verification event. The tier follows the gateway's identity decision — not merely whether an id string was supplied — and a caller your policy denies is refused the catalog read outright.

Settlement: do not fulfill on a placeholder

confirm_purchase today returns a non-settling placeholder and does not yet verify the payment-mandate signature(it checks the mandate's amount/currency against the authoritative total only). No money moves until the payment harness ships. Do not fulfill irreversibly on it.

Gate fulfilment on the explicit top-level settlement state, never on success alone (success: true means the confirm operation succeeded — verification granted — not that payment was captured) and never on the presence of a receipt or txHash:

{
  "success": true,
  "settled": false,
  "settlementStatus": "unsettled_placeholder",
  "txHash": "astrasync-placeholder:...",
  "txHashKind": "astrasync-correlation-placeholder"
}
// Fulfil only when:  settled === true  (settlementStatus: "settled")

Pre-harness, settled is always false. When the harness lands it flips to true with a real txHash. Relatedly, the commerce.payment.mandate_invalid denial dimension fires on amount/currency only today; signature-invalid joins it when the harness lands — do not assume signature validation exists yet.

Direct path (Path A): you MUST authorize the cart value before settling

The middleware (createMiddleware) verifies the agent's identity and route access — it does notenforce the agent's spending limits, because it never sees your priced cart total (that exists only in your handler, after pricing). If you run your own checkout and settle without a value check, the agent's PDLSS spend limits are unenforced — it can settle any amount. This is mandatory, not optional.

Before you settle, call authorizeSettlement with your authoritative priced total (never an agent-supplied amount — that is spoofable). It is fail-closed: settlement is refused unless the value verifies against the agent's limits.

import { authorizeSettlement } from '@astrasyncai/verification-gateway';

// in your checkout handler, AFTER you have priced the cart:
const decision = await authorizeSettlement(gatewayConfig, {
  agentId,            // the verified ASTRA-id of the caller
  value: cartTotal,   // YOUR authoritative priced total (not the agent's claim)
  currency: 'USD',
});
if (!decision.authorized) {
  // step-up band, at/above the Hard Limit, missing value, or a verify error
  return refuse(decision.reason, decision.failures);
}
// decision.authorized === true  ->  safe to capture / settle

authorized is true only on a clean grant. A value in the step-up/approval band, a value at or above the agent's Hard Limit, a missing value, or a verify-access error all return authorized: false. The bridge does exactly this inside confirm_purchase; on the direct path it is your responsibility — do not skip it.

Five-minute path to first success

  1. Get an API key at /dashboard/api-keys (or the staging dashboard for testing).
  2. Register your inbound surface as an endpoint at /dashboard/endpoints. You'll get an ASTRAE-id and a one-time-visible webhook secret.
  3. Required: configure per-route policies on the endpoint detail page. Skipping this leaves your endpoint in pass-through mode — every request reaches your handler ungated. The dashboard surfaces a red banner on any active endpoint with no routes configured.
  4. Required for domain merchants (SDK 3.1.0+): map each route to its PDLSS purpose/action pair. An UNMAPPED route makes the middleware send the generic data/data.* defaults (derived from the HTTP method) — which DENIES agents that registered with your domain vocabulary, because data.read is not in their allowedActions. Set the Purpose and Action columns in the routes editor:
    # Commerce shop
    /api/catalog     GET   → purpose: shopping   action: shopping.search
    /api/checkout/*  POST  → purpose: shopping   action: shopping.purchase
    
    # Trading exchange (custom category — first-class)
    /api/orders      POST  → purpose: trading    action: trading.execute
    /api/positions   GET   → purpose: trading    action: trading.read
    Your agents then register with allowedActions: ['trading.execute', 'trading.read'] (categories derive automatically). The mapping is authoritative over agent-supplied headers and propagates to running SDKs within the routes refresh interval (~5 min). MCP-surface merchants use the equivalent toolGates config (per-tool purpose/action) in the MCP adapter. See the canonical vocabulary table below.
  5. Set your endpoint's unverifiedAgentPolicy (deny / audit / allow_partial / allow_full) on the endpoint detail page. This decides what happens when an unregistered agent hits you. audit (v2.3.8+) lets the request through but adds an X-Astra-Unverified-Warning response header so you can soft-launch without blocking traffic. This is the endpoint-side inboundpolicy — distinct from the PDLSS scope's outbound unverifiedCounterpartyPolicy (inbound vs outbound explained).
  6. Clone the worked example, edit .env, npm install, npm start. (See next section.)
  7. Hit the running server with curl using a registered X-Astra-AgentId header — see the Test your integration page for the four common request shapes and their literal responses.
Purpose category (bare noun)Actions (dotted verbs)Covers
shoppingshopping.search, shopping.purchaseCatalog browse/search; checkout create + confirm (one purchase intent)
identityidentity.verify, identity.lookupTrust/score verification; identity resolution
discoverydiscovery.readMerchant listing, docs search
datadata.read, data.write, data.deleteGeneric API access — the fallback pair for unmapped routes (data.execute is reserved)
custom (e.g. trading, accounting)trading.execute, accounting.read, …Any bare noun + dotted verbs under it — first-class, not second-class
  • allowedActions is required (min 1) — the evaluator is fail-closed on actions. categories is derived from the dotted-action prefixes; supply extras only to widen.
  • MCP tool names (list_products) are valid enumerated actions; they derive no category, so add an explicit one if all your actions are dotless.
  • Transport verbs (GET/POST) never travel as actions — HTTP senders map them through the pinned table (GET → data.read, POST/PUT/PATCH → data.write, DELETE → data.delete).
  • Retired gen-1 tokens (read_data, write_data, execute_action) are rejected at registration — use the data category.

Worked example

A self-contained Express server (≈100 lines) using the gateway. Clone, edit env, run. Lives in the monorepo at examples/express-verification-gateway/.

git clone https://github.com/AstraSync-KYA/KYA-Platform.git
cd KYA-Platform/examples/express-verification-gateway
cp .env.example .env  # then set ASTRASYNC_API_KEY
npm install
npm start             # listens on :4001
Browse the example on GitHub

Defense-in-depth at the Express edge

The SDK route-pattern matcher is now case-insensitive by default in v2.4.13+ (audit F-A6-31). For belt-and-braces defense — especially if you're running an older SDK version or a mixed-edge setup — set Express to reject mixed-case path variants at the router layer too:

// app.ts (mount BEFORE you load the verification-gateway middleware)
app.set('case sensitive routing', true);
app.set('strict routing', true);

With these set, requests like GET /api/ADMIN/usersreturn 404 from Express before the gateway middleware is consulted — closes the bypass class even on SDK versions where the matcher hasn't been updated.

Staging ↔ Production promotion

Staging and production share the same request and response shapes — promoting a tested integration is a single config change.

EnvironmentapiBaseUrl
Staginghttps://staging.astrasync.ai/api
Productionhttps://astrasync.ai/api

Swap the host segment (staging.astrasync.ai astrasync.ai). Everything else — path shape, request bodies, response schemas, error envelopes — is identical. SDK config stays the same.

OpenAPI spec — tier-filtered, authenticate to see more

GET /api/docs/openapi.json serves a tier-filtered view. Anonymous callers see the public-tier subset (~15 paths: auth, registration, verify-access). Pass Authorization: Bearer kya_… to get the paths your tier can actually call (endpoint CRUD, allowlist management, agent CRUD, etc.).

The response stamps x-astrasync-tier-available-counts on the root so you can see at a glance how many additional paths your tier is missing — e.g. if you see customer-admin: 18 and you're unauthenticated, that's 18 paths waiting behind your API key.

Allowlist semantics + per-entry scope fields

Empty allowlist = open (round-12 default flip). Verified agents proceed to PDLSS / trust-score / attestation gates. To CONSTRAIN access, populate the allowlist with explicit owner identifiers — only listed owners can call the endpoint, everyone else is denied at the allowlist gate before downstream evaluation.

POST /api/endpoints/{id}/allowlist accepts these body fields:

FieldShapeEffect
allowedOwnerIdentifierASTRAD-* or ASTRAO-*Public-form owner id of the developer (ASTRAD) or org (ASTRAO). Preferred.
allowedPurposesstring[]Restrict this allowlist entry to specific PDLSS purpose categories — BARE NOUNS (shopping, data, trading), never dotted action tokens (vocabulary-unification round). Since v2.4.10 (round-18.5 F3): when a request carries a purpose, the entry must enumerate it explicitly — empty/omitted now denies (fail-closed).
minTrustScoreinteger (0-100)Override the endpoint baseline for this entry. Higher floor for a specific owner.
expiresAtISO-8601 date-timeAuto-expire the entry. Useful for time-limited grants.

Full schema in the authenticated OpenAPI spec — GET /api/docs/openapi.json with your Authorization: Bearer kya_… header.

When verify-access returns a denial

When a denial response carries a failures[].dimension starting with pdlss. (e.g. pdlss.action, pdlss.scope.resources, pdlss.selfInstantiation) the agent's PDLSS doesn't cover the request. The agent needs to update its PDLSS — see Updating an agent's PDLSS for the two-step retire+re-register recovery workflow available today via the Agent dashboard.

Denial dimensions to recognise on your side:

  • pdlss.* — agent-policy failure; recovery is on the agent side (retire+re-register with broader PDLSS).
  • counterparty.allowlist— agent isn't in your endpoint's allowlist; you add them (or the endpoint owner does).
  • counterparty.trust— agent's trust score below your endpoint's requirement; the agent needs to accrue trust signals (or you lower the threshold).
  • attestation.* — agent or owner missing a required attestation (e.g. verified_human_party); complete the attestation flow.
  • pdlss.limits.hard_limit_required— the agent declared no Hard Limit (per-transaction ceiling), so it can't transact value. A configuration issue, not an overspend — the agent re-registers with a Hard Limit.
  • commerce.intent.approval_required— the value is in the approval band (≥ Autonomous Limit, < Hard Limit) and needs human approval, which isn't wired yet, so it is blocked. NOTE: this is an allowed-but-escalate outcome — policyAllowed stays true. Gate on recommendation / requiresApproval, never admit a request on policyAllowed alone. The SDK middleware already fails closed on this; bridge confirm_purchase returns settlementStatus: blocked_step_up_required with no receipt.
Don't echo trust internals back to the agent.The verify-access result you receive carries the agent's trust score (for your minTrustScore gating) and a verificationContext.dynamicTrustScore. These are relying-party data — an agent must never see its own score (a gaming vector). Do not surface either field in a response body or error you return to the calling agent.

The four config knobs that matter

FieldWhat it does
apiBaseUrlAstraSync API root. Always includes /api. Prod: https://astrasync.ai/api; staging: https://staging.astrasync.ai/api. The SDK runs an init-time HEAD probe and warns if you've pointed it at a marketing 404.
apiKeyYour kya_* bearer token, issued at /dashboard/api-keys. The SDK authenticates as the merchant on every verify-access call. Treat this as a secret — keep it in env vars, not source.
routesPer-pattern policy. { pattern, method, minAccessLevel }. Top-down match — first hit wins. minAccessLevel is one of none, guidance, read-only, standard, full, internal. Maps to canonical trust-score thresholds (cheatsheet on the Verification Gateway page).
counterpartyTypeThe shape of your endpoint — api, mcp_server, website, agent, other. AstraSync uses this to categorise inbound traffic in the activity feed.

Optional: counterpartyId pins the SDK to a specific ASTRAE-id (use it when one merchant operates multiple endpoints under the same origin). disableInitChecks skips the startup HEAD probe (handy in tests).

Register your endpoint (worked example)

Round-14 (BREAKING — May 2026): the body field renamed from endpointUrl to counterpartyUrl for consistency with verify-access, dashboard policy, and SDK config. Both POST (create) and PUT (update) accept only the new name; the strict-mode validator returns a clean 400 unrecognized_keys naming counterpartyUrl for partners sending the old field.

Create:

curl -X POST https://astrasync.ai/api/endpoints \
  -H "Authorization: Bearer kya_..." \
  -H "Content-Type: application/json" \
  -d '{
    "counterpartyType": "api",
    "name": "My Checkout API",
    "counterpartyUrl": "https://merchant.example/api",
    "catalogUrl": "https://merchant.example/api/catalog",   // surfaced by discover_catalog()
    "skillUrl": "https://merchant.example/skill.md",        // referenced from list_merchants()
    "discoverableByPlatformAgents": true,  // browse: appear in list_merchants() (default false)
    "routableViaBridge": true,             // transact: start_checkout/discover_catalog (default true)
    "trustScoreRequirement": 40,
    "requiresVerification": true,
    "requiresRuntimeChallenge": false,
    "unverifiedAgentPolicy": "deny"
  }'

For a bridge-routable Path B merchant, set catalogUrl, skillUrl, and the two bridge flags. The flags are independent: discoverableByPlatformAgents controls browse listing in list_merchants() (default false); routableViaBridge controls whether the bridge routes transactions to you (default true) — a registered endpoint with routableViaBridge:false returns merchant_not_bridge_routable. Note supportedProtocols is read-only/derived (from metadata) — it is not a settable field on this request.

Update (PUT — partial; only fields you send):

curl -X PUT https://astrasync.ai/api/endpoints/ASTRAE-... \
  -H "Authorization: Bearer <DASHBOARD_JWT>" \
  -H "Content-Type: application/json" \
  -d '{ "counterpartyUrl": "https://merchant.example/api/v2" }'

Note: PUT is dashboard-only post-round-13 (R13-3). API-key auth on PUT returns 403 policy_mutation_requires_dashboard — use a dashboard JWT.

Per-protocol terminal status

These literals come from each protocol's published specification, not from AstraSync's choice. AstraSync proxies the protocol's terminal state verbatim onto the response — completed for protocols that define completed as the success terminal, settled for MPP's settlement semantics, authorized for the two-step authorize/capture protocols inherited from card-payment conventions. Normalising would mean lying about the protocol contract; partners check against the protocol's own published terminal state.

ProtocolSuccessful terminal status
acpcompleted
ap2completed
a2acompleted
ucpcompleted
vicompleted
mppsettled
agent-payauthorized (two-step: auth then capture)
tapauthorized (two-step: auth then capture)

agent-pay and TAP use a two-step authorize/capture flow standard in card payments. The first response carries status: 'authorized'; the merchant captures funds in a follow-up step. Treating 'authorized' as "not yet successful" will surface false negatives. Check against the protocol-specific terminal status, not a single hardcoded literal.

A2A request shape (JSON-RPC envelope)

Six of the eight commerce protocols accept {line_items, buyer} at the top level. A2A is the exception — it wraps the same payload in a JSON-RPC 2.0 envelope. Worked curl example:

curl -X POST https://merchant.example/api/commerce/a2a \
  -H "Authorization: Bearer kya_..." \
  -H "X-Astra-Id: ASTRA-..." \
  -H "X-Astra-Purpose: shopping.purchase" \
  -H "X-Astra-Action: complete_purchase" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "complete_purchase",
    "params": {
      "line_items": [
        { "sku": "WIDGET-001", "quantity": 1, "unit_price_cents": 1999 }
      ],
      "buyer": { "email": "[email protected]" }
    },
    "id": "req-2026-05-14-001"
  }'

The other 7 protocols (acp / ap2 / mpp / ucp / vi / agent-pay / tap) accept the same {line_items, buyer} shape directly at the body root, without the JSON-RPC envelope. Send the wrapped envelope only on the A2A endpoint.

Where to next