Skip to content
Open
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
5 changes: 5 additions & 0 deletions typescript/.changeset/thegraph-action-provider.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
55 changes: 55 additions & 0 deletions typescript/agentkit/src/action-providers/thegraph/README.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions typescript/agentkit/src/action-providers/thegraph/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions typescript/agentkit/src/action-providers/thegraph/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./theGraphActionProvider";
export * from "./schemas";
export * from "./constants";
export * from "./queries";
124 changes: 124 additions & 0 deletions typescript/agentkit/src/action-providers/thegraph/queries.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): 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<Record<string, unknown>> =
(raw as { data?: { subgraphMetadataSearch?: Array<Record<string, unknown>> } })?.data
?.subgraphMetadataSearch ?? [];
const hits: SubgraphHit[] = metas.map(m => {
const sg = ((m.subgraphs as Array<Record<string, unknown>>) || [])[0] || null;
const dep =
((sg?.currentVersion as Record<string, unknown>)?.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;
}
49 changes: 49 additions & 0 deletions typescript/agentkit/src/action-providers/thegraph/schemas.ts
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading
Loading