Every GraphQL query pays its own fare.
Price each operation by what it selects and charge for it over MPP (HTTP 402).
Live demo · Quick start · How it works · Pricing rules · Security
Try it in the browser at fareql.pages.dev. Type a query and the meter quotes it live. Pay with a demo method and you get the data and a receipt. No money moves. Shortcuts: pay for a search · slide a page size · try to ride for free
Verified on Tempo mainnet: a real
tempo.chargepayment of 0.001 USDC.e through FareQL, settled on chain:0xc1f2a9f1…28bd(testnet:0x4fcba990…d346).
A GraphQL API is one endpoint, so a flat price per route doesn't fit it: { hello } and { users(first: 100) { edges { node { email } } } } hit the same URL. FareQL works out the fare of every operation from your schema and charges it through mppx with Tempo, Stripe or any other MPP method. Clients (apps, scripts, AI agents) pay per question. They don't need an account, an API key or a plan, and the money goes straight to your address.
| Schema-driven prices | @price(amount: "0.001") on fields and types, multiplied through lists, Relay connections and @listSize |
| One Challenge per operation | Bound to the exact document and variables. A Credential for a cheap query can't pay for an expensive one |
| Charged after validation | Persisted queries and depth limits run first, so nobody pays for an operation the server refuses |
| Fails closed | If FareQL can't read or price an operation, the operation doesn't run |
| Red-teamed | About 100 attack tests: parser differentials, cache bypasses, fragment bombs, scope collisions |
| Works where you are | GraphQL Yoga, Apollo Server 4/5 and any Fetch handler (Hono, Next.js, Workers), on graphql 16 and 17 |
npm install fareql mppx graphqlimport { createSchema, createYoga } from 'graphql-yoga'
import { Mppx, tempo } from 'mppx/server'
import { fareDirectiveTypeDefs } from 'fareql'
import { useFareql } from 'fareql/yoga'
const mppx = Mppx.create({
methods: [tempo.charge({ currency: '0x…' /* TIP-20 token */, recipient: '0x…' })],
})
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
${fareDirectiveTypeDefs}
type Query {
hello: String
weather(city: String!): Forecast @price(amount: "0.001")
search(query: String!, first: Int = 5): [Hit!]! @price(amount: "0.0005")
}
type Forecast { temperature: Float }
type Hit { title: String, snippet: String @price(amount: "0.0002") }
`,
resolvers,
}),
plugins: [useFareql({ charge: (o) => mppx.charge(o), pricing: { maxPrice: '1' } })],
}){ hello } is free. { search(query: "mpp", first: 2) { snippet } } costs 2 × (0.0005 + 0.0002) = 0.0014.
- You price your schema. Put
@price(amount: "0.001")on whatever costs money. - A client asks. FareQL prices the operation from what it selects. Free operations run as usual.
- It gets a quote. A priced operation answers
402 Payment Requiredwith an MPP Challenge for that exact operation, plus the price and its breakdown. - It pays and asks again. The client (an app, the
mppxCLI, an AI agent) pays, for example in stablecoins on Tempo, and resends the query with the Credential. - It gets the answer. FareQL verifies the Credential through mppx, runs the operation and returns the data with a
Payment-Receipt.
Shown in Quick start. Register useFareql after plugins that change what runs (APQ, persisted operations, validation rules). It hooks into execution, so it always prices the final document. A backstop before each response charges results that never reached execution, so a response cache or a plugin that short-circuits can't serve a priced result for free.
Yoga-only options:
getRequest(context)supplies the HTTP request for transports that have none, such as graphql-ws. Without it, priced operations over those transports are refused.maxSubscriptionEvents(default 100) is how many events one payment buys on a priced subscription.
import { ApolloServer } from '@apollo/server'
import { fareqlApolloPlugin } from 'fareql/apollo'
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [fareqlApolloPlugin({ charge: (o) => mppx.charge(o) })],
})The plugin charges in didResolveOperation. By then Apollo has parsed, validated and resolved the operation (APQ included), and its response cache hasn't answered yet. In an HTTP batch only one priced operation can be paid. Operations run in-process with server.executeOperation() have no HTTP request and are not charged.
import { withPayment } from 'fareql'
export const POST = withPayment((req) => handler(req), { schema, charge: (o) => mppx.charge(o) })withPayment reads the request itself, so it refuses anything the wrapped handler could read differently:
- Content types are matched exactly and case-sensitively, as GraphQL Yoga does, so the gate never accepts a body the handler would refuse.
- It accepts GET and
application/jsonPOST by default. Enableapplication/graphql,application/graphql+json, form-urlencoded or multipart uploads withencodings: [...], but only if your handler accepts them too. - It refuses the following with a 4xx before anything is charged:
- URL fragments, duplicate or unknown parameters, and GraphQL parameters in a POST URL;
- non-object
variables, andquerysent together withdocumentId; - bodies on other methods, and mutations over GET.
maxBodyBytes(1 MB),maxQueryLength(100 000),maxTokens(15 000) andmaxBatchSize(10) bound the work.validationRulesadds your server's extra rules, so operations they would reject are refused before payment.- APQ works without a hash store in the gate: a hash-only request gets
PersistedQueryNotFoundand the full query the client resends is priced, so nobody pays for a hash the handler doesn't know. To price hashes or trusted documents (documentId) from your own store, passresolveDocument. - Batches are refused unless you set
batching: true. With it, the whole batch is charged in one payment andmaxPriceapplies to the total.
For full control, createPaymentGate(options)(request) returns one of free, payment-required, paid or rejected.
FareQL doesn't pick a network. It prices the operation and hands the amount to mppx, and your mppx configuration decides where it is paid.
// Tempo mainnet: USDC.e
tempo.charge({ currency: '0x20C000000000000000000000b9537d11c60E8b50', recipient: '0x…' })
// Moderato testnet: pathUSD
tempo.charge({ currency: '0x20c0000000000000000000000000000000000001', recipient: '0x…', testnet: true })- The examples and the playground run on mainnet by default. Set
TEMPO_NETWORK=testnetto use Moderato. - Clients read the network from the Challenge's
chainId.- The mppx CLI uses mainnet by default. Add
--network testnetto pay a testnet API. - Testnet funds come from
npx mppx account fund.
- The mppx CLI uses mainnet by default. Add
| Rule | Behaviour |
|---|---|
Field @price |
Charged once per resolved instance of the field |
Type @price |
Applies to every field returning that type, unless the field has its own |
| Field selected several times | Charged once per response key, as the executor resolves it |
| Lists | Children × list size from first, last, limit, take, top, pageSize or perPage (also inside an input object, e.g. page: { limit }), otherwise defaultListSize (10). Capped at maxListSize (100). null or unbounded counts as maxListSize; negative sizes are refused |
| Nested lists | [[Cell]]: each inner level multiplies by defaultListSize |
| Connections and page wrappers | A non-list field given a size (users(first: 50)) passes it to the lists it selects (edges, nodes, items…). Restrict with connections: { sizedFields }, or turn it off with false |
@listSize |
As in the IBM GraphQL Cost Directives draft: slicingArguments, sizedFields, assumedSize, requireOneSlicingArgument |
| Interfaces and unions | Every possible runtime type is priced and the most expensive one is charged. A @price on an interface field or type applies to its implementations |
| Code-first schemas | extensions: { price: '0.001' } or extensions: { directives: { price: { amount: '0.001' } } } |
Fragments, @skip, @include |
Followed and evaluated with the request variables |
__typename, introspection |
Free |
maxPrice |
Operations above it get PRICE_LIMIT_EXCEEDED before any Challenge |
maxFieldVisits, maxDepth |
Operations too complex to price get TOO_COMPLEX (10 000 selections, 100 levels) |
| No price anywhere | A schema with no @price at all (e.g. one that lost its directives) is refused with NO_PRICES instead of making everything free |
Other options:
decimals(default 6) must match your payment method's token.defaultFieldPrice,listSizeArguments,connections.fieldPrice(ctx)prices fields in code.
priceOperation() is exported for quotes and tests.
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment id="…", realm="api.example.com", method="tempo", intent="charge", request="…"
Content-Type: application/json; charset=utf-8
{
"errors": [{
"message": "Payment required: this operation costs 0.0014",
"extensions": {
"code": "PAYMENT_REQUIRED",
"price": {
"amount": "0.0014",
"breakdown": [
{ "path": "search", "unitAmount": "0.0005", "count": "2", "amount": "0.001" },
{ "path": "search.snippet", "unitAmount": "0.0002", "count": "2", "amount": "0.0004" }
]
}
}
}]
}Clients need nothing from FareQL, only an MPP client. With the mppx CLI, an account and a little USDC.e on Tempo:
npx mppx account create # then send the account a little USDC.e
npx mppx https://api.example.com/graphql -X POST -H 'content-type: application/json' \
-d '{"query":"{ search(query: \"mpp\", first: 2) { snippet } }"}'For an API on the Moderato testnet, fund the account with npx mppx account fund and add --network testnet.
In code, mppx/client wraps fetch, so 402s are paid and retried automatically. The paid response carries a Payment-Receipt and Cache-Control: private, so shared caches never store paid content. Set exposePrice: false to keep prices out of error bodies.
- Binding. Each Challenge's
scopeis a hash of the printed document, the operation name and the declared variables. mppx keeps it in the HMAC-protected Challenge and refuses a Credential whose scope differs. - Price what runs. Yoga and Apollo charge after the server's own parsing, validation and persisted-document lookup.
withPaymentrefuses requests a handler could parse differently. - Fail closed. Unreadable, unpriceable, over-limit or ambiguous requests are answered with an error and never run.
- Bounded work. Pricing is linear in the document, like graphql-js field collection. Token, length, depth, body and batch limits keep it cheap.
- Tested by attack.
test/attacks-*.test.tshold the red-team suites. Each test asserts the secure behaviour.
Shared options
| Option | |
|---|---|
charge |
(o) => mppx.charge(o), or (o) => mppx.tempo.charge(o) when several methods are configured. Receives { amount, scope, description } |
pricing |
See Pricing rules |
describe(op, price) |
Challenge description. Default: GraphQL <operation name> |
exposePrice |
Include the price breakdown in 402 bodies. Default: true |
Notes and limits
- Payment settles before execution, so a resolver error after payment is not refunded. Keep priced resolvers reliable.
- Subscriptions are charged once, when they start, for up to
maxSubscriptionEventsevents. Metered streams over MPP sessions are planned. - Set
maxListSizeto your server's real page cap. If the server returns more items thanmaxListSize, the client is under-charged, because larger requested sizes are clamped. - In production, give
tempo.charge()a sharedstoreso a Credential can't be replayed across instances.
examples/playground: the live demo. Run it locally withnpx tsx examples/playground/server.ts. SetRECIPIENTandMPP_SECRET_KEYfor real payments on Tempo mainnet, plusTEMPO_NETWORK=testnetfor the Moderato testnet.examples/yoga-tempo: a minimal GraphQL Yoga server on Tempo (mainnet by default,TEMPO_NETWORK=testnetfor Moderato).examples/apollo-tempo: Apollo Server with a priced Relay-style connection.
npm test # pricing, gate, Yoga, Apollo, attacks, real mppx loops
npm run build && node scripts/smoke.mjs # the built package over real HTTP in plain NodeMIT