From 2beb6179408fea6751156626002aeb9a6be35436 Mon Sep 17 00:00:00 2001 From: PaulieB14 <94752445+PaulieB14@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:13:08 -0400 Subject: [PATCH] feat(action-providers): add The Graph provider (query subgraphs directly, pay-per-query over x402) Adds a `thegraph` action provider that lets an agent query The Graph directly and pay per query over x402 from its own wallet: search_subgraphs, get_subgraph_schema, query_subgraph. Query prices are variable, so each action probes the 402 price and enforces a per-query maxPaymentUsdc cap before signing. Base-only; reuses AgentKit's existing x402 signing stack (no new deps). Discovery + query construction ported from PayQL (Apache-2.0). Includes README, unit tests, and a changeset. Complements the managed `graphadvocate` provider with a direct/self-serve tier. --- .../.changeset/thegraph-action-provider.md | 5 + .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/thegraph/README.md | 55 +++ .../action-providers/thegraph/constants.ts | 27 ++ .../src/action-providers/thegraph/index.ts | 4 + .../src/action-providers/thegraph/queries.ts | 124 ++++++ .../src/action-providers/thegraph/schemas.ts | 49 +++ .../thegraph/theGraphActionProvider.test.ts | 82 ++++ .../thegraph/theGraphActionProvider.ts | 398 ++++++++++++++++++ 9 files changed, 745 insertions(+) create mode 100644 typescript/.changeset/thegraph-action-provider.md create mode 100644 typescript/agentkit/src/action-providers/thegraph/README.md create mode 100644 typescript/agentkit/src/action-providers/thegraph/constants.ts create mode 100644 typescript/agentkit/src/action-providers/thegraph/index.ts create mode 100644 typescript/agentkit/src/action-providers/thegraph/queries.ts create mode 100644 typescript/agentkit/src/action-providers/thegraph/schemas.ts create mode 100644 typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.ts diff --git a/typescript/.changeset/thegraph-action-provider.md b/typescript/.changeset/thegraph-action-provider.md new file mode 100644 index 000000000..9f32315a9 --- /dev/null +++ b/typescript/.changeset/thegraph-action-provider.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": minor +--- + +Add The Graph action provider: query any subgraph on The Graph directly and pay per query over x402 from the agent's own wallet (search_subgraphs, get_subgraph_schema, query_subgraph). Base-only, with a per-query spend cap enforced before payment. Discovery/query logic ported from PayQL; reuses the existing x402 stack, so no new dependencies. diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 9f7164086..bfc5cfa05 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -26,6 +26,7 @@ export * from "./opensea"; export * from "./spl"; export * from "./superfluid"; export * from "./sushi"; +export * from "./thegraph"; export * from "./truemarkets"; export * from "./twitter"; export * from "./wallet"; diff --git a/typescript/agentkit/src/action-providers/thegraph/README.md b/typescript/agentkit/src/action-providers/thegraph/README.md new file mode 100644 index 000000000..b9a7e5316 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/README.md @@ -0,0 +1,55 @@ +# The Graph Action Provider + +This directory provides an action provider for querying [The Graph](https://thegraph.com) directly from an agent, paying **per query** over x402 from the agent's own wallet. It exposes discovery, schema introspection, and paid GraphQL query actions across every subgraph on the network — no API key, no signup. + +The discovery + query-construction logic is ported from [PayQL](https://github.com/PaulieB14/payql) (Apache-2.0); payment is signed with the agent's AgentKit wallet. + +## Actions + +| Action | What it does | Cost | +|---|---|---| +| `search_subgraphs` | Find the best subgraph(s) for a plain-English topic (id, name, signal) | ~$0.01 USDC (free if a registry is configured) | +| `get_subgraph_schema` | List a subgraph's queryable entities + arguments | ~$0.01 USDC | +| `query_subgraph` | Run a GraphQL query against a subgraph | per-query gateway price (~$0.01) | + +Typical flow: `search_subgraphs` → `get_subgraph_schema` → `query_subgraph`. + +## How payment works + +Query prices are variable, so each paid action **probes the gateway's 402 price first, checks it against `maxPaymentUsdc`, and only then signs and pays** — a query priced above the cap is refused before any signature. Payments settle in USDC on Base using the same x402 signing stack as the built-in `x402` provider, so **no new dependencies**. + +## Wallet Providers + +Requires an `EvmWalletProvider` (payments settle in USDC on Base). Base mainnet only. + +## Configuration + +```typescript +import { theGraphActionProvider } from "@coinbase/agentkit"; + +const provider = theGraphActionProvider({ + // Per-query spend ceiling in whole USDC (default THE_GRAPH_MAX_PAYMENT_USDC env, then 1.0). + maxPaymentUsdc: 0.05, + // Optional free discovery endpoint (e.g. a curated subgraph registry). If unset, + // search_subgraphs runs a tiny paid query against The Graph's network subgraph. + // registryUrl: "https://...", +}); +``` + +## Example + +``` +User: What are the 5 most recent swaps on Uniswap v3 (Arbitrum)? +Agent: + 1. search_subgraphs({ query: "uniswap v3 arbitrum" }) -> picks a subgraph id + 2. get_subgraph_schema({ subgraphId }) -> sees `swaps` entity + 3. query_subgraph({ subgraphId, query: "{ swaps(first: 5, orderBy: timestamp, orderDirection: desc) { amountUSD } }" }) + -> data, paid ~$0.01 USDC on Base from the agent's wallet +``` + +## Relationship to the `graphadvocate` provider + +Two tiers over the same protocol: + +- **`graphadvocate`** — managed / no setup. The agent pays Graph Advocate, which routes and queries and returns the answer. +- **`thegraph`** (this provider) — self-serve / bring-your-own. The agent queries The Graph gateway **directly** and pays its own way per query. Cheaper, more control, and it drives direct usage of The Graph. diff --git a/typescript/agentkit/src/action-providers/thegraph/constants.ts b/typescript/agentkit/src/action-providers/thegraph/constants.ts new file mode 100644 index 000000000..c4339b0d7 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/constants.ts @@ -0,0 +1,27 @@ +/** + * The Graph's live x402 gateway. Subgraph queries settle in USDC on Base per call. + */ +export const GRAPH_X402_GATEWAY_BASE = "https://gateway.thegraph.com/api/x402"; + +/** + * The Graph Network metadata subgraph (indexes every subgraph, version and + * deployment). Used for keyword discovery + popularity ranking. + */ +export const GRAPH_NETWORK_SUBGRAPH_ID = "DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp"; + +/** + * USDC on Base (the asset x402 payments settle in). + */ +export const GRAPH_USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; + +/** + * Payments settle on Base, so the provider only supports Base mainnet. + */ +export const THE_GRAPH_SUPPORTED_NETWORKS = ["base-mainnet"]; + +/** + * Per-query spend ceiling in whole USDC. A query whose 402 price exceeds this is + * refused before any payment is signed. Typical gateway price is ~$0.01. + * Overridable via config or the THE_GRAPH_MAX_PAYMENT_USDC env var. + */ +export const DEFAULT_MAX_PAYMENT_USDC = 1.0; diff --git a/typescript/agentkit/src/action-providers/thegraph/index.ts b/typescript/agentkit/src/action-providers/thegraph/index.ts new file mode 100644 index 000000000..651bcfcab --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/index.ts @@ -0,0 +1,4 @@ +export * from "./theGraphActionProvider"; +export * from "./schemas"; +export * from "./constants"; +export * from "./queries"; diff --git a/typescript/agentkit/src/action-providers/thegraph/queries.ts b/typescript/agentkit/src/action-providers/thegraph/queries.ts new file mode 100644 index 000000000..0034b2055 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/queries.ts @@ -0,0 +1,124 @@ +/** + * Query-construction and parsing helpers for The Graph. + * The discovery + introspection logic is ported from PayQL (Apache-2.0). + */ + +/** + * Fulltext discovery against The Graph network subgraph's `subgraphMetadataSearch` + * index (over displayName + description), hopping to the most-signalled active + * subgraph and its current deployment. + */ +export const SEARCH_QUERY = `query AgentkitGraphSearch($text: String!, $first: Int!) { + subgraphMetadataSearch(text: $text, first: $first) { + displayName + description + categories + subgraphs(first: 1, where: { active: true }, orderBy: currentSignalledTokens, orderDirection: desc) { + id + active + currentSignalledTokens + currentVersion { + subgraphDeployment { + ipfsHash + stakedTokens + signalledTokens + queryFeesAmount + } + } + } + } +}`; + +/** + * Introspects a subgraph's top-level query entities and their arguments. + */ +export const INTROSPECT_QUERY = `query AgentkitGraphIntrospect { __type(name: "Query") { fields { name args { name } } } }`; + +/** + * Turns a free-text query into a Postgres tsquery with prefix matching: + * "uniswap v3" -> "uniswap:* & v3:*". + * + * @param q - The free-text search string + * @returns A tsquery string + */ +export function toFulltext(q: string): string { + const tokens = q + .trim() + .toLowerCase() + .replace(/[^a-z0-9 ]/g, " ") + .split(/\s+/) + .filter(Boolean); + if (!tokens.length) return q.trim(); + return tokens.map(t => `${t}:*`).join(" & "); +} + +/** + * Builds a GraphQL POST request body. + * + * @param query - The GraphQL query string + * @param variables - Optional GraphQL variables + * @returns A fetch RequestInit for the query + */ +export function gqlBody(query: string, variables?: Record): RequestInit { + return { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ query, variables: variables ?? {} }), + }; +} + +/** + * Converts a wei string to a GRT float. + * + * @param v - The wei value as a string + * @returns The GRT amount, or null + */ +export function weiToGRT(v: string | null | undefined): number | null { + if (v == null) return null; + const n = Number(v); + return Number.isFinite(n) ? n / 1e18 : null; +} + +/** + * A single subgraph discovery result. + */ +export interface SubgraphHit { + displayName: string | null; + subgraphId: string | null; + ipfsHash: string | null; + currentSignalledTokensGRT: number | null; + queryFeesGRT: number | null; + description: string | null; + categories: string[] | null; +} + +/** + * Parses a `subgraphMetadataSearch` GraphQL response into ranked hits. + * + * @param raw - The parsed GraphQL response body ({ data, errors }) + * @returns Subgraph hits sorted by signal, descending + */ +export function parseHits(raw: unknown): SubgraphHit[] { + const metas: Array> = + (raw as { data?: { subgraphMetadataSearch?: Array> } })?.data + ?.subgraphMetadataSearch ?? []; + const hits: SubgraphHit[] = metas.map(m => { + const sg = ((m.subgraphs as Array>) || [])[0] || null; + const dep = + ((sg?.currentVersion as Record)?.subgraphDeployment as Record< + string, + unknown + >) ?? null; + return { + displayName: (m.displayName as string) ?? null, + subgraphId: (sg?.id as string) ?? null, + ipfsHash: (dep?.ipfsHash as string) ?? null, + currentSignalledTokensGRT: weiToGRT(sg?.currentSignalledTokens as string), + queryFeesGRT: weiToGRT(dep?.queryFeesAmount as string), + description: (m.description as string) ?? null, + categories: (m.categories as string[]) ?? null, + }; + }); + hits.sort((a, b) => (b.currentSignalledTokensGRT ?? 0) - (a.currentSignalledTokensGRT ?? 0)); + return hits; +} diff --git a/typescript/agentkit/src/action-providers/thegraph/schemas.ts b/typescript/agentkit/src/action-providers/thegraph/schemas.ts new file mode 100644 index 000000000..0c1c79872 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/schemas.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +/** + * Input schema for discovering subgraphs by plain-English topic. + */ +export const SearchSubgraphsSchema = z + .object({ + query: z + .string() + .min(1, "A search topic is required.") + .describe("Plain-English topic to find subgraphs for, e.g. 'uniswap v3 arbitrum' or 'aave lending'"), + first: z + .number() + .int() + .min(1) + .max(20) + .optional() + .describe("How many subgraph candidates to return (default 5)"), + }) + .describe("Find the best subgraphs on The Graph for a data topic"); + +/** + * Input schema for fetching a subgraph's queryable entities/fields. + */ +export const GetSubgraphSchemaSchema = z + .object({ + subgraphId: z + .string() + .min(1) + .describe("The subgraph id (from search_subgraphs) whose schema to introspect"), + }) + .describe("Get the queryable entities and their arguments for a subgraph"); + +/** + * Input schema for running a paid GraphQL query against a subgraph. + */ +export const QuerySubgraphSchema = z + .object({ + subgraphId: z.string().min(1).describe("The subgraph id to query (from search_subgraphs)"), + query: z + .string() + .min(1) + .describe("The GraphQL query string (use get_subgraph_schema to see available entities/fields)"), + variables: z + .record(z.string(), z.unknown()) + .optional() + .describe("Optional GraphQL variables"), + }) + .describe("Run a GraphQL query against a subgraph, auto-paying the x402 fee"); diff --git a/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.test.ts b/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.test.ts new file mode 100644 index 000000000..95a8a5729 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.test.ts @@ -0,0 +1,82 @@ +import { theGraphActionProvider, TheGraphActionProvider } from "./theGraphActionProvider"; +import { toFulltext, parseHits } from "./queries"; +import { Network } from "../../network"; + +describe("TheGraphActionProvider", () => { + let provider: TheGraphActionProvider; + + beforeEach(() => { + provider = theGraphActionProvider({ maxPaymentUsdc: 0.05 }); + }); + + describe("supportsNetwork", () => { + it("supports base-mainnet", () => { + expect(provider.supportsNetwork({ networkId: "base-mainnet" } as Network)).toBe(true); + }); + + it("does not support other networks", () => { + expect(provider.supportsNetwork({ networkId: "ethereum-mainnet" } as Network)).toBe(false); + expect(provider.supportsNetwork({ networkId: "base-sepolia" } as Network)).toBe(false); + }); + }); + + describe("wallet provider guard", () => { + it("rejects a non-EVM wallet provider before any network call", async () => { + const result = await provider.querySubgraph( + {} as never, // not an EvmWalletProvider + { subgraphId: "5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV", query: "{ _meta { block { number } } }" }, + ); + const parsed = JSON.parse(result); + expect(parsed.error).toBe(true); + expect(parsed.message).toBe("Unsupported wallet provider"); + }); + }); + + describe("toFulltext", () => { + it("builds a prefix tsquery from free text", () => { + expect(toFulltext("uniswap v3")).toBe("uniswap:* & v3:*"); + expect(toFulltext("Aave, Lending!")).toBe("aave:* & lending:*"); + }); + }); + + describe("parseHits", () => { + it("maps and sorts subgraphMetadataSearch results by signal", () => { + const raw = { + data: { + subgraphMetadataSearch: [ + { + displayName: "Low", + description: "d1", + categories: ["defi"], + subgraphs: [ + { + id: "sgLow", + active: true, + currentSignalledTokens: String(1e18), + currentVersion: { subgraphDeployment: { ipfsHash: "Qm1", queryFeesAmount: String(2e18) } }, + }, + ], + }, + { + displayName: "High", + description: "d2", + categories: ["nft"], + subgraphs: [ + { + id: "sgHigh", + active: true, + currentSignalledTokens: String(9e18), + currentVersion: { subgraphDeployment: { ipfsHash: "Qm2", queryFeesAmount: String(3e18) } }, + }, + ], + }, + ], + }, + }; + const hits = parseHits(raw); + expect(hits.map(h => h.subgraphId)).toEqual(["sgHigh", "sgLow"]); + expect(hits[0].currentSignalledTokensGRT).toBe(9); + expect(hits[0].queryFeesGRT).toBe(3); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.ts b/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.ts new file mode 100644 index 000000000..68596e739 --- /dev/null +++ b/typescript/agentkit/src/action-providers/thegraph/theGraphActionProvider.ts @@ -0,0 +1,398 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { EvmWalletProvider, WalletProvider } from "../../wallet-providers"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { SearchSubgraphsSchema, GetSubgraphSchemaSchema, QuerySubgraphSchema } from "./schemas"; +import { SEARCH_QUERY, INTROSPECT_QUERY, toFulltext, gqlBody, parseHits } from "./queries"; +import { + GRAPH_X402_GATEWAY_BASE, + GRAPH_NETWORK_SUBGRAPH_ID, + GRAPH_USDC_BASE, + THE_GRAPH_SUPPORTED_NETWORKS, + DEFAULT_MAX_PAYMENT_USDC, +} from "./constants"; + +/** + * Configuration for the TheGraphActionProvider. + */ +export interface TheGraphConfig { + /** + * Per-query spend ceiling in whole USDC. A query whose 402 price exceeds this + * is refused before any payment is signed. Defaults to the + * THE_GRAPH_MAX_PAYMENT_USDC env var, then 1.0 USDC. Typical query is ~$0.01. + */ + maxPaymentUsdc?: number; + + /** + * Optional free GraphQL endpoint for discovery (search_subgraphs), e.g. a + * curated subgraph registry. If unset, discovery runs a tiny paid x402 query + * against The Graph's network subgraph. + */ + registryUrl?: string; + + /** + * Override the x402 gateway base URL. + */ + gatewayBase?: string; +} + +type PaidOutcome = + | { ok: true; data: unknown; paidUsd: number } + | { ok: false; error: Record }; + +/** + * TheGraphActionProvider lets an agent query The Graph directly and pay per + * query from its own wallet over x402. It exposes discovery, schema + * introspection, and paid GraphQL query actions across every subgraph on the + * network. Discovery + query construction is ported from PayQL (Apache-2.0); + * payment is signed with the agent's AgentKit wallet. + * + * @augments ActionProvider + */ +export class TheGraphActionProvider extends ActionProvider { + private readonly maxPaymentUsdc: number; + private readonly registryUrl?: string; + private readonly gatewayBase: string; + + /** + * Constructor for the TheGraphActionProvider. + * + * @param config - Configuration options for the provider + */ + constructor(config: TheGraphConfig = {}) { + super("thegraph", []); + this.maxPaymentUsdc = + config.maxPaymentUsdc ?? + parseFloat(process.env.THE_GRAPH_MAX_PAYMENT_USDC ?? String(DEFAULT_MAX_PAYMENT_USDC)); + this.registryUrl = config.registryUrl ?? process.env.THE_GRAPH_REGISTRY_URL; + this.gatewayBase = config.gatewayBase ?? GRAPH_X402_GATEWAY_BASE; + } + + /** + * Discovers the best subgraphs for a plain-English data topic. + * + * @param walletProvider - The wallet provider (used to pay when no free registry is configured) + * @param args - The search topic and optional result count + * @returns A JSON string of ranked subgraph candidates, or an error + */ + @CreateAction({ + name: "search_subgraphs", + description: ` +Finds the best subgraphs on The Graph for a plain-English data topic. Returns +ranked candidates (subgraph id, name, description, signal, query URL) so you can +pick one to introspect and query. + +Costs a tiny x402 fee (~$0.01 USDC on Base) unless a free discovery registry is +configured. Use this first, then get_subgraph_schema, then query_subgraph. + +Input: { "query": "uniswap v3 arbitrum", "first": 5 }`, + schema: SearchSubgraphsSchema, + }) + async searchSubgraphs( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + const body = gqlBody(SEARCH_QUERY, { text: toFulltext(args.query), first: args.first ?? 5 }); + + // Free curated discovery source, if configured — no payment. + if (this.registryUrl) { + try { + const res = await fetch(this.registryUrl, body); + const raw = await res.json(); + return JSON.stringify({ success: true, source: "registry", subgraphs: parseHits(raw) }, null, 2); + } catch (error) { + return this.errString("discovery via registry failed", error); + } + } + + // Default: a tiny paid query against The Graph's network subgraph. + const outcome = await this.paidGraphFetch(walletProvider, this.subgraphUrl(GRAPH_NETWORK_SUBGRAPH_ID), body); + if (!outcome.ok) return JSON.stringify(outcome.error, null, 2); + return JSON.stringify( + { success: true, source: "x402", paidUsd: outcome.paidUsd, subgraphs: parseHits(outcome.data) }, + null, + 2, + ); + } + + /** + * Returns a subgraph's queryable entities and their arguments. + * + * @param walletProvider - The wallet provider used to pay the x402 fee + * @param args - The subgraph id to introspect + * @returns A JSON string of entities/args, or an error + */ + @CreateAction({ + name: "get_subgraph_schema", + description: ` +Returns a subgraph's top-level queryable entities and their arguments, so you can +write a correct query. Costs a tiny x402 fee (~$0.01 USDC on Base). + +Input: { "subgraphId": "5zvR82..." }`, + schema: GetSubgraphSchemaSchema, + }) + async getSubgraphSchema( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + const outcome = await this.paidGraphFetch( + walletProvider, + this.subgraphUrl(args.subgraphId), + gqlBody(INTROSPECT_QUERY), + ); + if (!outcome.ok) return JSON.stringify(outcome.error, null, 2); + const fields = + ((outcome.data as { data?: { __type?: { fields?: Array> } } })?.data + ?.__type?.fields) ?? []; + const entities = fields + .filter(f => typeof f.name === "string" && !(f.name as string).startsWith("_")) + .map(f => ({ + name: f.name as string, + args: ((f.args as Array<{ name: string }>) ?? []).map(a => a.name), + })); + return JSON.stringify({ success: true, paidUsd: outcome.paidUsd, entities }, null, 2); + } + + /** + * Runs a paid GraphQL query against a subgraph. + * + * @param walletProvider - The wallet provider used to pay the x402 fee + * @param args - The subgraph id, GraphQL query, and optional variables + * @returns A JSON string of the query result, or an error + */ + @CreateAction({ + name: "query_subgraph", + description: ` +Runs a GraphQL query against a subgraph and auto-pays the per-query x402 fee +(~$0.01 USDC on Base) from the agent's wallet. Use get_subgraph_schema first to +see available entities/fields. A query priced above the configured +maxPaymentUsdc is refused before any payment. + +Input: { "subgraphId": "5zvR82...", "query": "{ tokens(first: 5) { id symbol } }" }`, + schema: QuerySubgraphSchema, + }) + async querySubgraph( + walletProvider: WalletProvider, + args: z.infer, + ): Promise { + const outcome = await this.paidGraphFetch( + walletProvider, + this.subgraphUrl(args.subgraphId), + gqlBody(args.query, args.variables), + ); + if (!outcome.ok) return JSON.stringify(outcome.error, null, 2); + return JSON.stringify( + { success: true, subgraphId: args.subgraphId, paidUsd: outcome.paidUsd, data: outcome.data }, + null, + 2, + ); + } + + /** + * Checks if the action provider supports the given network. + * Payments settle in USDC on Base. + * + * @param network - The network to check + * @returns True if the network is Base mainnet + */ + supportsNetwork = (network: Network) => + (THE_GRAPH_SUPPORTED_NETWORKS as readonly string[]).includes(network.networkId!); + + /** + * Builds the x402 gateway URL for a subgraph. + * + * @param subgraphId - The subgraph id + * @returns The full gateway URL + */ + private subgraphUrl(subgraphId: string): string { + return `${this.gatewayBase}/subgraphs/id/${subgraphId}`; + } + + /** + * Fetches a GraphQL request against the x402 gateway: probes the 402 price, + * enforces the per-query cap, and only then signs and pays from the wallet. + * + * @param walletProvider - The wallet provider used to sign the payment + * @param url - The subgraph gateway URL + * @param body - The GraphQL request body + * @returns A structured outcome with the parsed data or an error + */ + private async paidGraphFetch( + walletProvider: WalletProvider, + url: string, + body: RequestInit, + ): Promise { + if (!(walletProvider instanceof EvmWalletProvider)) { + return { + ok: false, + error: { + error: true, + message: "Unsupported wallet provider", + details: "The Graph x402 payments settle in USDC on Base and require an EvmWalletProvider.", + }, + }; + } + + // Probe (unpaid) to read the 402 price before committing to a payment. + let pre: Response; + try { + pre = await fetch(url, body); + } catch (error) { + return { ok: false, error: { error: true, message: "Request failed", details: String(error) } }; + } + + // No payment required (e.g. free registry, or an error page). + if (pre.status !== 402) { + const data = await pre.json().catch(() => null); + return { ok: true, data, paidUsd: 0 }; + } + + const accepts = await this.parse402(pre); + const option = this.pickCheapestUsdc(accepts); + if (!option) { + return { + ok: false, + error: { error: true, message: "No USDC-on-Base payment option offered by the gateway" }, + }; + } + + const priceUsd = Number(option.maxAmountRequired ?? option.amount ?? 0) / 1e6; + if (!(priceUsd <= this.maxPaymentUsdc)) { + return { + ok: false, + error: { + error: true, + message: "Payment exceeds limit", + details: `Query price $${priceUsd} exceeds the configured maxPaymentUsdc of $${this.maxPaymentUsdc}.`, + priceUsd, + maxPaymentUsdc: this.maxPaymentUsdc, + }, + }; + } + + // Within cap — sign and pay with the agent's wallet. + try { + const client = await this.createX402Client(walletProvider); + const fetchWithPayment = wrapFetchWithPayment(fetch, client); + const paid = await fetchWithPayment(url, body); + const data = await paid.json().catch(() => null); + if (paid.status !== 200) { + return { + ok: false, + error: { + error: true, + message: `Query failed with status ${paid.status}. Payment was not settled.`, + data, + }, + }; + } + return { ok: true, data, paidUsd: priceUsd }; + } catch (error) { + return { ok: false, error: { error: true, message: "Paid query failed", details: String(error) } }; + } + } + + /** + * Parses the x402 payment requirements from a 402 response (header first, then body). + * + * @param res - The 402 fetch Response + * @returns The `accepts` array of payment options + */ + private async parse402(res: Response): Promise>> { + let accepts: Array> = []; + const header = res.headers.get("payment-required") ?? res.headers.get("x-payment-required"); + if (header) { + try { + accepts = (JSON.parse(atob(header)).accepts as Array>) ?? []; + } catch { + // fall through to body + } + } + if (accepts.length === 0) { + const bodyJson = (await res.json().catch(() => ({}))) as { accepts?: Array> }; + accepts = bodyJson.accepts ?? []; + } + return accepts; + } + + /** + * Picks the cheapest `exact`/USDC-on-Base payment option from a 402's accepts. + * + * @param accepts - The payment options from the 402 + * @returns The cheapest matching option, or undefined + */ + private pickCheapestUsdc( + accepts: Array>, + ): Record | undefined { + if (!Array.isArray(accepts) || accepts.length === 0) return undefined; + const exact = accepts.filter(a => String(a.scheme ?? "").toLowerCase() === "exact"); + const pool = exact.length ? exact : accepts; + const usdc = pool.filter(a => String(a.asset ?? "").toLowerCase() === GRAPH_USDC_BASE.toLowerCase()); + const finalPool = usdc.length ? usdc : pool; + const onBase = finalPool.filter(a => { + const n = String(a.network ?? "").toLowerCase(); + return n === "eip155:8453" || n === "base" || n === "base-mainnet" || n === "8453"; + }); + const candidates = onBase.length ? onBase : finalPool; + return [...candidates].sort( + (a, b) => + Number(a.maxAmountRequired ?? a.amount ?? 0) - Number(b.maxAmountRequired ?? b.amount ?? 0), + )[0]; + } + + /** + * Creates an x402 client configured to sign payments with the agent's wallet. + * Mirrors AgentKit's x402 provider so signing behaves identically. + * + * @param walletProvider - The EVM wallet provider to sign with + * @returns A configured x402Client + */ + private async createX402Client(walletProvider: EvmWalletProvider): Promise { + const client = new x402Client(); + const account = walletProvider.toSigner(); + const signer = { + ...account, + readContract: (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }) => + walletProvider.readContract({ + address: args.address, + abi: args.abi as never, + functionName: args.functionName as never, + args: args.args as never, + }), + }; + registerExactEvmScheme(client, { signer }); + return client; + } + + /** + * Formats a caught error as a JSON string. + * + * @param message - A human message + * @param error - The caught error + * @returns A JSON error string + */ + private errString(message: string, error: unknown): string { + return JSON.stringify( + { error: true, message, details: error instanceof Error ? error.message : String(error) }, + null, + 2, + ); + } +} + +/** + * Factory function to create a new TheGraphActionProvider instance. + * + * @param config - Configuration options for the provider + * @returns A new TheGraphActionProvider + */ +export const theGraphActionProvider = (config: TheGraphConfig = {}) => + new TheGraphActionProvider(config);