Skip to content

feat(graphql): operation-level x402 gating, query complexity costing, and targeted pubsub routing (#358, #361, #373) - #459

Open
sparkysolutions10-pixel wants to merge 1 commit into
Divinity-Labs:mainfrom
sparkysolutions10-pixel:feat/graphql-costing-gating-pubsub-358-361-373
Open

sparkysolutions10-pixel wants to merge 1 commit into
Divinity-Labs:mainfrom
sparkysolutions10-pixel:feat/graphql-costing-gating-pubsub-358-361-373

Conversation

@sparkysolutions10-pixel

Copy link
Copy Markdown
Contributor

Summary of Changes

This PR resolves GraphQL security, economic protection, query costing, and real-time streaming performance across three issues:

  1. Operation-Level x402 Gating & Dynamic Pricing (GraphQL serves the paid REST data for free #358):

    • Evaluates GraphQL query documents dynamically at the AST level rather than relying on static path matching.
    • Declares per-field pricing in GATED_GRAPHQL_FIELDS (getPrice: $0.10, getBestRoute: $0.05, getPriceHistory: $0.05, pools: $0.05, etc.).
    • Computes cumulative price in cents across all requested gated fields, scaling with repeated aliases (e.g. 2 aliased getPrice calls require $0.20; getPrice + getBestRoute requires $0.15).
    • Rejects unpaid queries for gated fields with HTTP 402 in preHandler before any resolver executes or queries the database/cache.
    • Unpaid metadata/introspection queries (listPairs, pairs, __schema, __type) are explicitly free and return 200 without payment.
    • Integrates with x402 facilitator verification and API key quota enforcement.
    • Subscription Extension Note: WebSocket subscriptions via graphql-transport-ws receive authentication during connection_init (e.g. API key or pre-authorized token) and debit per-event delivery or upfront reserve against GATED_GRAPHQL_FIELDS.
  2. Query Complexity & Cost Limits (One GraphQL request costs one rate-limit token however much it asks for #361):

    • Assigns complexity cost weights in GRAPHQL_FIELD_COSTS (e.g. expensive resolvers getPrice: 5, getBestRoute: 10, getPriceHistory: 10 vs lightweight listPairs: 1, introspection: 0).
    • Pre-execution AST cost evaluation via calculateQueryCost ensuring 50 aliased getPrice calls cost 250 while 1 call costs 5.
    • Implements costLimitRule validation rule against configurable GRAPHQL_MAX_COST (default: 100), rejecting excessive queries before resolver invocation with structured QUERY_TOO_COMPLEX error.
    • Documented in .env.example.
  3. Targeted PubSub Routing & Subscription Metrics (Every subscriber is woken by every price #373):

    • Partitions PubSub topics by pair and network: price_updated:${pair}:${network} and pair-wide price_updated:${pair}:all.
    • Ingesters publish to targeted topics so subscribers only wake when their watched pair receives updates, eliminating wasted filter callbacks across non-matching subscribers.
    • Retains full backward compatibility for network-less subscriptions to receive all networks for the watched pair.
    • Adds graphql_active_subscriptions Prometheus gauge metric tracking live subscriptions by pair and network, automatically incrementing on subscribe and decrementing on complete/disconnect.

Benchmark Measurement (#373)

  • Simulated 1,000 active subscribers across 5 pairs (200 subscribers per pair) receiving 100 price updates for XLM/USDC:
    • Before (single global topic): 100 updates × 1,000 subscribers = 100,000 filter callback invocations.
    • After (targeted topic routing): 100 updates × 200 matching subscribers = 20,000 filter callback invocations (80% reduction in callback overhead; $O(M)$ instead of $O(N)$).

Testing & Verification

  • Added test suite src/__tests__/graphqlCostingGatingPubsub.test.ts (13 tests) verifying all 3 issues.
  • All 94 tests across 6 related GraphQL and x402 test files pass cleanly (graphqlCostingGatingPubsub.test.ts, graphqlSubscription.test.ts, graphqlErrorMasking.test.ts, graphqlPairResolution.test.ts, x402.test.ts, x402-quota.test.ts).
  • Verified TypeScript compilation (tsc --noEmit) with zero errors.

Closes #358, Closes #361, Closes #373

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: one issue per PR, and only issues assigned to you

This is a process requirement, applied to all 23 of your open PRs in this repo. It is not a judgement about the code, which I have not been able to review — and that is the first half of the problem.

The bundling

Across your open PRs, 150 distinct issues are claimed. This one claims several. #483 claims 22, #481 claims 20, #477 claims 20, #474 claims 20.

A PR that closes twenty issues cannot be reviewed. Each of those issues has its own acceptance criteria — typically five — so twenty issues is a hundred separate things to verify in one diff, and if any one of them is wrong the whole PR is blocked while it is fixed. Every batch I published says the issues are deliberately independent so they can be worked in parallel, by different people, without colliding. Bundling them removes exactly that property.

The assignments

135 of the 150 issues claimed are assigned to other contributors — 67 different people.

This PR is an example: it names issues currently assigned to other contributors who are working on them now. Several have open PRs of their own. If this merged as it stands, those people would have done the work and received nothing for it, and I would have no way to tell the two efforts apart.

I am not assuming anything about why. Assignments change, Drips and GitHub can disagree, and a wide-ranging branch picks up issue numbers easily. But the effect on the people holding those assignments is the same regardless of intent, which is why I have to act on it.

What I need

  • One issue per PR. The issue number in the title, its acceptance criteria met, and nothing else in the diff.
  • Only issues assigned to you, or unassigned ones you have claimed through Drips. If an issue is assigned to someone else, it is theirs.
  • Re-open the work that meets both conditions as individual PRs. I will review them in the order they arrive and at the same pace as everyone else's.

Nothing is being closed. These stay open and are yours to re-cut, withdraw or split as you choose.

Two things worth knowing before you re-cut

Some of this has already landed. #488 (SSRF guard for bazaar_search) and #487 (the paid_call host allow-list) cover the same ground as your #449; #486 covers the MCP README. Check current main before rebuilding something that is already there.

main has moved a long way today — #306, #309, #252, #256, #355, #381, #390, #402, #476, #488 and more. Cut from current main, not from where these branches started.

If you believe an issue named here is genuinely yours, say which and I will check the assignment myself — I would rather be corrected than have someone lose work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants