From dc15758038b23b8285256c64a8db150a74de733c Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 5 Aug 2026 11:27:16 +0300 Subject: [PATCH] fix(x402): read v2 resource description from the top-level field The discovery filter read v2 descriptions only from metadata.description. The CDP discovery API returns them at the top level, so filterByDescription dropped every v2 resource and discover_x402_services returned v1 results only. Reading the top-level field first (metadata.description kept as a fallback) restores discovery for v2 resources. v1 handling is unchanged. Signed-off-by: Oleg --- .../x402-discovery-v2-description.md | 5 ++ .../src/action-providers/x402/constants.ts | 2 + .../src/action-providers/x402/utils.test.ts | 85 +++++++++++++++++++ .../src/action-providers/x402/utils.ts | 6 +- 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 typescript/.changeset/x402-discovery-v2-description.md create mode 100644 typescript/agentkit/src/action-providers/x402/utils.test.ts diff --git a/typescript/.changeset/x402-discovery-v2-description.md b/typescript/.changeset/x402-discovery-v2-description.md new file mode 100644 index 000000000..30dbc64d6 --- /dev/null +++ b/typescript/.changeset/x402-discovery-v2-description.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Fixed x402 service discovery dropping every v2 resource: descriptions are read from the top-level `description` field returned by the discovery API, with `metadata.description` kept as a fallback diff --git a/typescript/agentkit/src/action-providers/x402/constants.ts b/typescript/agentkit/src/action-providers/x402/constants.ts index 961491dd3..1f9a8bf32 100644 --- a/typescript/agentkit/src/action-providers/x402/constants.ts +++ b/typescript/agentkit/src/action-providers/x402/constants.ts @@ -67,6 +67,8 @@ export interface DiscoveryResource { url?: string; resource?: string; type?: string; + /** v2: the CDP discovery API returns the resource description at the top level */ + description?: string; metadata?: { [key: string]: unknown; description?: string; diff --git a/typescript/agentkit/src/action-providers/x402/utils.test.ts b/typescript/agentkit/src/action-providers/x402/utils.test.ts new file mode 100644 index 000000000..fa31b4151 --- /dev/null +++ b/typescript/agentkit/src/action-providers/x402/utils.test.ts @@ -0,0 +1,85 @@ +import { filterByDescription, filterByKeyword } from "./utils"; +import { DiscoveryResource } from "./constants"; + +/** + * The CDP discovery API returns v2 resources with the description at the TOP LEVEL + * (`resource.description`), not under `metadata`. These tests pin both shapes so + * discovery keeps working whichever one a facilitator returns. + */ +describe("x402 discovery utilities", () => { + const v2TopLevel: DiscoveryResource = { + resource: "https://example.com/v2-top-level", + x402Version: 2, + description: "Executable swap quote at size, including price impact and slippage", + }; + + const v2Metadata: DiscoveryResource = { + resource: "https://example.com/v2-metadata", + x402Version: 2, + metadata: { description: "Token metadata lookup for ERC-20 contracts" }, + }; + + const v2NoDescription: DiscoveryResource = { + resource: "https://example.com/v2-none", + x402Version: 2, + }; + + const v2Default: DiscoveryResource = { + resource: "https://example.com/v2-default", + x402Version: 2, + description: "Access to protected content", + }; + + const v1: DiscoveryResource = { + resource: "https://example.com/v1", + x402Version: 1, + accepts: [ + { + scheme: "exact", + network: "base", + asset: "0x0000000000000000000000000000000000000001", + description: "Weather forecast for a given city", + }, + ], + }; + + describe("filterByDescription", () => { + it("keeps v2 resources that carry the description at the top level", () => { + expect(filterByDescription([v2TopLevel])).toEqual([v2TopLevel]); + }); + + it("keeps v2 resources that carry the description under metadata", () => { + expect(filterByDescription([v2Metadata])).toEqual([v2Metadata]); + }); + + it("prefers the top-level description when both are present", () => { + const both: DiscoveryResource = { + ...v2TopLevel, + metadata: { description: "metadata description" }, + }; + expect(filterByKeyword([both], "price impact")).toEqual([both]); + }); + + it("drops v2 resources with no description in either place", () => { + expect(filterByDescription([v2NoDescription])).toEqual([]); + }); + + it("drops the default placeholder description", () => { + expect(filterByDescription([v2Default])).toEqual([]); + }); + + it("still keeps v1 resources with a description in accepts[]", () => { + expect(filterByDescription([v1])).toEqual([v1]); + }); + }); + + describe("filterByKeyword", () => { + it("matches keywords in a top-level v2 description", () => { + expect(filterByKeyword([v2TopLevel, v2Metadata], "slippage")).toEqual([v2TopLevel]); + }); + + it("matches keywords in a metadata v2 description", () => { + expect(filterByKeyword([v2TopLevel, v2Metadata], "erc-20")).toEqual([v2Metadata]); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/x402/utils.ts b/typescript/agentkit/src/action-providers/x402/utils.ts index 356316687..8d918ad32 100644 --- a/typescript/agentkit/src/action-providers/x402/utils.ts +++ b/typescript/agentkit/src/action-providers/x402/utils.ts @@ -180,13 +180,17 @@ export function filterByNetwork( /** * Extracts description from a resource based on its x402 version. * - v1: description is in accepts[].description - * - v2: description is in metadata.description + * - v2: description is at the top level, with metadata.description as a fallback * * @param resource - The discovery resource * @returns The description string or empty string if not found */ function getResourceDescription(resource: DiscoveryResource): string { if (resource.x402Version === 2) { + const topLevelDesc = resource.description; + if (typeof topLevelDesc === "string" && topLevelDesc.trim()) { + return topLevelDesc; + } const metadataDesc = resource.metadata?.description; return typeof metadataDesc === "string" ? metadataDesc : ""; }