Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-score/commerce",
"version": "2.9.2",
"version": "2.9.3",
"description": "Agentic commerce SDK: identity middleware (Hono, Express, Fastify, Next.js, Web Fetch) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agentic commerce.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion src/challenge/agent_instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const X402_WARNING =
'Do NOT send USDC manually to the deposit addresses. Use `agentscore-pay pay` so the credential is signed and submitted; otherwise the request will not complete even though the deposit lands.';

const TEMPO_TOOL = '`tempo request` for Tempo USDC';
const AGENTSCORE_PAY_TOOL = '`agentscore-pay` — Base + Solana + Tempo from one CLI';
const AGENTSCORE_PAY_TOOL = '`agentscore-pay`, one CLI for Base, Solana and Tempo';

const DEFAULT_WALLET_COMPATIBILITY =
'Any client that can produce a valid MPP credential (Authorization: Payment) or x402 X-Payment header. Use the CLI commands above; sign-it-yourself is also fine.';
Expand Down
2 changes: 1 addition & 1 deletion src/challenge/how_to_pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function buildHowToPay({
stripe.what_it_does_link_cli =
'Mints a one-time-use SharedPaymentToken scoped to this purchase (user approves in Link wallet), then submits it as the payment credential.';
} else if (linkCliBlocked) {
stripe.note = `link-cli SPT path not available for this purchase Stripe link-cli caps spend requests at $500.00 ($50000 cents); your total is $${totalNum}. Use your own Stripe account with the SharedPaymentToken API instead.`;
stripe.note = `link-cli SPT path not available for this purchase: Stripe link-cli caps spend requests at $500.00 ($50000 cents); your total is $${totalNum}. Use your own Stripe account with the SharedPaymentToken API instead.`;
}
block.stripe = stripe;
}
Expand Down
4 changes: 2 additions & 2 deletions src/checkout.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* High-level Checkout orchestrator — composes 402-emit + verify+settle.
*
* The Checkout primitive collapses the agent-commerce dance (emit 402 →
* The Checkout primitive collapses the agentic-commerce dance (emit 402 →
* verify+settle on retry → respond) into a single `await checkout.handle(request)`
* call. It services every merchant shape:
*
Expand Down Expand Up @@ -832,7 +832,7 @@ function resolveIssuerPolicy(
}

/**
* High-level agent-commerce orchestrator.
* High-level agentic-commerce orchestrator.
*
* @example
* ```ts
Expand Down
6 changes: 3 additions & 3 deletions src/discovery/agentscore_content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@ export function buildAgentscoreOnboardingSteps(opts: {
'Install agentscore-pay if you don\'t already have a compatible client for your funded chain: ' +
'`npm i -g @agent-score/pay` (or `brew install agentscore/tap/agentscore-pay`). ' +
`${merchantName} accepts: ${railsHuman}. agentscore-pay speaks every supported rail; ` +
(compatibleFragment ? `the rails table also lists per-rail \`compatible_clients\` ${compatibleFragment}. ` : '') +
(compatibleFragment ? `the rails table also lists per-rail \`compatible_clients\`: ${compatibleFragment}. ` : '') +
'Any spec-compliant client for an individual rail works too.';
const bootstrapStep =
'First-run only: bootstrap wallet + Passport. Run `agentscore-pay agent-guide --json` ' +
'for the canonical cold-start path it walks `agentscore-pay init` ' +
'for the canonical cold-start path; it walks `agentscore-pay init` ' +
'(creates keystore + per-chain wallet), `agentscore-pay passport login` ' +
`(one-time KYC${requiresKyc ? '; required for this merchant' : ''}; the human completes a verify URL once and pay caches the operator_token), ` +
'and `agentscore-pay balance` to see which chain has USDC. Skip if your wallet+Passport are already provisioned.';
const stripeFallbackStep =
'If your only payment method is a Stripe / Link card (no crypto), install `@stripe/link-cli` ' +
'instead of agentscore-pay and use it on the SPT rail. Identity gating still applies the ' +
'instead of agentscore-pay and use it on the SPT rail. Identity gating still applies: the ' +
'merchant\'s 403 with `verify_url` lets you bootstrap a Passport even with no crypto wallet involved.';
const returningUserStep =
'Returning user note: if you\'ve paid an AgentScore-gated merchant before from this wallet, ' +
Expand Down
2 changes: 1 addition & 1 deletion src/discovery/bazaar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function createBazaarDiscovery(config: BazaarDiscoveryConfig): Prom
/* v8 ignore start -- peer-dep-absence guard; @x402/extensions is installed in test env */
if (!bazaar?.declareDiscoveryExtension) {
throw new Error(
'@x402/extensions not installed `npm install @x402/extensions` for createBazaarDiscovery.',
'@x402/extensions not installed. Run `npm install @x402/extensions` for createBazaarDiscovery.',
);
}
/* v8 ignore stop */
Expand Down
18 changes: 9 additions & 9 deletions src/discovery/llms_txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function llmsTxtIdentitySection({
return '';
}
const aipBullet = aip
? '\n- **`Agent-Identity: <JWT>` + RFC 9421 signature** present an Agent Identity Token (AIP) ' +
? '\n- **`Agent-Identity: <JWT>` + RFC 9421 signature**: present an Agent Identity Token (AIP) ' +
'from a trusted issuer (AgentScore is always trusted). Short-lived and bound to your key; sign ' +
'the request (`Signature-Input` + `Signature` over `@method @authority @path agent-identity`, ' +
'tag `agent-identity`) to prove possession. No long-lived token on the wire. Mint one with ' +
Expand All @@ -46,11 +46,11 @@ export function llmsTxtIdentitySection({
: '';
return `## Identity

AgentScore identity is reusable across every AgentScore-gated merchant one KYC, no re-verification per site. Pick a header:
AgentScore identity is reusable across every AgentScore-gated merchant: one KYC, no re-verification per site. Pick a header:

- **\`X-Wallet-Address: 0x...\` or base58** works on signing rails (Tempo, x402, Solana MPP). The wallet you claim must sign the payment.
- **\`X-Operator-Token: opc_...\`** works on every rail, including Stripe SPT. Reusable across AgentScore merchants until expiry.${aipBullet}
- **Neither** you get a 403 with \`verify_url\`. Complete the session flow once and reuse the resulting \`opc_...\` everywhere.${complianceNote}`;
- **\`X-Wallet-Address: 0x...\` or base58**: works on signing rails (Tempo, x402, Solana MPP). The wallet you claim must sign the payment.
- **\`X-Operator-Token: opc_...\`**: works on every rail, including Stripe SPT. Reusable across AgentScore merchants until expiry.${aipBullet}
- **Neither**: you get a 403 with \`verify_url\`. Complete the session flow once and reuse the resulting \`opc_...\` everywhere.${complianceNote}`;
}

interface LlmsTxtPaymentSectionConfig {
Expand Down Expand Up @@ -94,16 +94,16 @@ function llmsTxtPaymentSectionCompact(input: LlmsTxtPaymentSectionConfig): strin
const lines: string[] = ['## Payment', ''];
const rails = input.rails;
if (hasRailFamily(rails, 'tempo-')) {
lines.push('- **Tempo USDC via MPP** `tempo request -X POST -H "X-Operator-Token: opc_..." --json \'{...}\' --max-spend N ' + input.appUrl + '`');
lines.push('- **Tempo USDC via MPP**: `tempo request -X POST -H "X-Operator-Token: opc_..." --json \'{...}\' --max-spend N ' + input.appUrl + '`');
}
if (hasRailFamily(rails, 'x402-base-')) {
lines.push('- **x402 USDC on Base** (EIP-3009) `agentscore-pay pay POST ' + input.appUrl + ' --chain base -H "X-Operator-Token: opc_..." -d \'{...}\'`');
lines.push('- **x402 USDC on Base** (EIP-3009): `agentscore-pay pay POST ' + input.appUrl + ' --chain base -H "X-Operator-Token: opc_..." -d \'{...}\'`');
}
if (hasRailFamily(rails, 'mpp-solana-')) {
lines.push('- **USDC on Solana** `agentscore-pay pay POST ' + input.appUrl + ' --chain solana -H "X-Operator-Token: opc_..." -d \'{...}\'`');
lines.push('- **USDC on Solana**: `agentscore-pay pay POST ' + input.appUrl + ' --chain solana -H "X-Operator-Token: opc_..." -d \'{...}\'`');
}
if (rails.includes('stripe-spt')) {
lines.push('- **Stripe Shared Payment Token** agent mints SPT (own Stripe account scoped to networkId, OR `link-cli spend-request create --credential-type shared_payment_token --network-id <profileId> ...`)');
lines.push('- **Stripe Shared Payment Token**: agent mints SPT (own Stripe account scoped to networkId, OR `link-cli spend-request create --credential-type shared_payment_token --network-id <profileId> ...`)');
}
lines.push('');
lines.push('IMPORTANT: Do NOT use raw on-chain transfers. Use the CLI commands above so the payment credential is signed and submitted via the protocol handshake.');
Expand Down
4 changes: 2 additions & 2 deletions src/discovery/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function agentscoreDenialSchemas(): Record<string, unknown> {
'payment_required',
],
description:
"Denial code emitted by AgentScore's gate middleware in 403 responses. Every code carries a structured agent_instructions block describing recovery actions (per-code action: missing_identity → probe_identity_then_session, identity_verification_required / token_expired → deliver_verify_url_and_poll, invalid_credential → switch_token_or_restart_session, wallet_signer_mismatch → resign_or_switch_to_operator_token, wallet_auth_requires_wallet_signing → switch_to_operator_token, wallet_not_trusted → contact_support UNFIXABLE compliance only (sanctions/age/jurisdiction_restricted); fixable reasons re-route to identity_verification_required, payment_required → contact_merchant).",
"Denial code emitted by AgentScore's gate middleware in 403 responses. Every code carries a structured agent_instructions block describing recovery actions (per-code action: missing_identity → probe_identity_then_session, identity_verification_required / token_expired → deliver_verify_url_and_poll, invalid_credential → switch_token_or_restart_session, wallet_signer_mismatch → resign_or_switch_to_operator_token, wallet_auth_requires_wallet_signing → switch_to_operator_token, wallet_not_trusted → contact_support, for UNFIXABLE compliance only (sanctions/age/jurisdiction_restricted); fixable reasons re-route to identity_verification_required, payment_required → contact_merchant).",
},
AgentScoreDenialBody: {
type: 'object',
Expand All @@ -101,7 +101,7 @@ export function agentscoreDenialSchemas(): Record<string, unknown> {
description:
'JSON-encoded { action, steps, user_message } block. Always present on every denial; agents parse this to learn how to recover (e.g., poll verify_url, switch headers, re-sign).',
},
verify_url: { type: 'string', format: 'uri', description: "Present for missing_identity / identity_verification_required / token_expired denials. Agent shares this with the user to complete KYC or claim a wallet. Not present on wallet_not_trusted (UNFIXABLE compliance re-verification won't change the outcome)." },
verify_url: { type: 'string', format: 'uri', description: 'Present for missing_identity / identity_verification_required / token_expired denials. Agent shares this with the user to complete KYC or claim a wallet. Not present on wallet_not_trusted (UNFIXABLE compliance, so re-verification will not change the outcome).' },
session_id: { type: 'string' },
poll_url: { type: 'string', format: 'uri' },
poll_secret: { type: 'string' },
Expand Down
12 changes: 6 additions & 6 deletions src/discovery/skill_md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const RAIL_NOTES: Record<RailKey, string> = {
tempo_mpp: 'USDC. Use `agentscore-pay pay --chain tempo` (or `tempo request`); MPP credential goes in `Authorization: Payment`.',
x402_base: 'USDC (EIP-3009). Use `agentscore-pay pay --chain base`; X-Payment header carries the signed credential.',
solana_mpp: 'USDC (SPL). Use `agentscore-pay pay --chain solana`; MPP credential goes in `Authorization: Payment`.',
stripe: 'Card via Link wallet. Use `@stripe/link-cli` `agentscore-pay` emits the handoff hint when this rail is picked.',
stripe: 'Card via Link wallet. Use `@stripe/link-cli`; `agentscore-pay` emits the handoff hint when this rail is picked.',
};

const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
Expand All @@ -144,7 +144,7 @@ function validateInput(input: BuildSkillMdInput): void {
}
if (!NAME_RE.test(input.name)) {
throw new Error(
`buildSkillMd: name "${input.name}" is invalid must be lowercase alphanumeric and hyphens, no leading/trailing/consecutive hyphens (agentskills.io spec)`,
`buildSkillMd: name "${input.name}" is invalid: must be lowercase alphanumeric and hyphens, no leading/trailing/consecutive hyphens (agentskills.io spec)`,
);
}
if (!input.description || input.description.length === 0) {
Expand Down Expand Up @@ -222,7 +222,7 @@ function paymentSection(input: BuildSkillMdInput): string {
}
const rows: string[] = ['| Rail | Notes | Compatible clients |', '|---|---|---|'];
for (const r of input.acceptedRails) {
const list = (clients[r] ?? []).join(', ') || '';
const list = (clients[r] ?? []).join(', ') || 'none';
rows.push(`| **${RAIL_LABELS[r]}** | ${RAIL_NOTES[r]} | ${list} |`);
}
return [
Expand Down Expand Up @@ -253,15 +253,15 @@ function identitySection(input: BuildSkillMdInput): string {
? '\n\nThis merchant accepts AIP Agent Identity Tokens. If you hold an AIT from a trusted issuer ' +
'(AgentScore is always trusted), present the JWT in an `Agent-Identity` header plus an RFC 9421 ' +
'HTTP Message Signature (`Signature-Input` + `Signature` over `@method @authority @path agent-identity`, ' +
'tag `agent-identity`) signed with the token-bound cnf key — it satisfies identity in one round trip, ' +
'tag `agent-identity`) signed with the token-bound cnf key, satisfying identity in one round trip, ' +
'no separate AgentScore credential needed. `agentscore-pay pay --identity aip` does this automatically.'
: '';
return [
'## Identity Prerequisite',
'',
`${reqLine}${aipNote}`,
'',
'Denial bodies carry an `agent_instructions` block describing the recovery action read the `action` field and follow it. See the identity-bootstrap skill for the canonical denial-code → action table.',
'Denial bodies carry an `agent_instructions` block describing the recovery action: read the `action` field and follow it. See the identity-bootstrap skill for the canonical denial-code → action table.',
].join('\n');
}

Expand Down Expand Up @@ -321,7 +321,7 @@ function titleBlock(input: BuildSkillMdInput): string {
}

/**
* Render an agentskills.io-compatible `skill.md` for an agent-commerce merchant.
* Render an agentskills.io-compatible `skill.md` for an agentic-commerce merchant.
*
* Output is YAML frontmatter (`name` / `description` / optional `license` /
* `compatibility` / `allowed-tools` / `metadata`) followed by markdown sections
Expand Down
7 changes: 5 additions & 2 deletions tests/discovery/skill_md.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,16 @@ describe('buildSkillMd', () => {
expect(out).not.toContain('Stripe Shared Payment Token');
});

it("renders '' when a rail's compatible-clients list is explicitly empty", () => {
it("renders 'none' when a rail's compatible-clients list is explicitly empty", () => {
const out = buildSkillMd({
...baseInput,
acceptedRails: ['x402_base'],
compatibleClients: { x402_base: [] },
});
expect(out).toMatch(/x402 on Base.+\| —/);
// 'none' rather than an em-dash: this table is served on every store's
// skill.md, and the org rule bars em-dashes on external surfaces.
expect(out).toMatch(/x402 on Base.+\| none/);
expect(out).not.toContain('—');
});
});

Expand Down