Public GraphQL Subgraphs
Public Soccerverse GraphQL subgraphs for SVC/Democrit trading, Xaya chain stats, and shop, referral and pricing data.
Soccerverse runs three read-only GraphQL subgraphs that index public Polygon events: the Democrit/SVC exchange, Xaya account names, and Soccerverse shop, referral and price data. You ask for exactly the fields you want and get them back in one request. There is no API key.
Most apps should start with the Datacentre REST API. Use a subgraph for data it does not return, such as SVC exchange trades and orders, who owns a Xaya name, and pack purchase and price history.
This page uses the hosted service on play.soccerverse.com. If you run your own node, see Running your own node.
Quick start
Run this in a terminal:
curl -sS 'https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq' \
-H 'Content-Type: application/json' \
--data '{"query":"{ _meta { block { number timestamp } deployment hasIndexingErrors } }"}'
You get back something like this (a real capture, formatted for reading):
{
"data": {
"_meta": {
"block": { "number": 94969961, "timestamp": 1791160093 },
"deployment": "QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq",
"hasIndexingErrors": false
}
}
}
This is a health check. block is the newest Polygon block the subgraph has indexed (timestamp is Unix seconds), deployment is the ID you asked for, and hasIndexingErrors should be false. The same query works on all three subgraphs, so it is also a quick way to check that an ID is still valid.
Endpoints
Each subgraph has one endpoint, https://play.soccerverse.com/graph/subgraphs/id/<ID>, where <ID> is the subgraph's deployment ID. Send queries with POST.
| Subgraph | Endpoint | What it holds |
|---|---|---|
Democrit/SVC (democrit-sv) | https://play.soccerverse.com/graph/subgraphs/id/QmahYLjXXeQajGieM1bqFgtgWCsGfsML8Na2Drratweq5f | The SVC exchange: trades, listed sell orders and their vaults |
Xaya stats (xaya-stats) | https://play.soccerverse.com/graph/subgraphs/id/QmaZXXhj5KpsveiCeQXme19yZParTxRsQzUnbX6ujLg5un | Xaya account names and owners, moves, registrations and profile data |
Shop, referrals and pricing (sv-subgraph) | https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq | Referrals, pack sales, club sale status and pricing, and WCHI/USDC/WETH prices |
IDs last checked on 2026-10-05. The previous sv-subgraph ID, QmWybA1iubUyQCR4F2irZrTNZaoremX8urRGe6f9gRPX6N, was retired on 2026-10-05; switch any integration that still uses it to the ID above.
The hosted service has no friendly names or aliases. Paths such as /graph/democrit-sv or /graph/subgraphs/name/democrit-sv do not work. The ID path is the only way in.
IDs change. An ID identifies one deployed version of a subgraph, so it changes when Soccerverse deploys a new version. If you get deployment ... does not exist or No data found for subgraph ..., come back to this page for the current ID. Keep the ID in one place in your code so an update is a one-line change.
Get the URL exactly right:
- Keep the
/graphprefix. Without it you get the website's 404 page. - Leave off any trailing slash. A URL ending in
/answers{"message":"Not found"}. - Use
POST. AGETdoes not run queries, and a browser that opens the URL ends on the website's 404 page. To open the query editor in a browser, add/graphql(see the tip below).
Tip: to try queries in a browser, open the endpoint URL with /graphql added, for example https://play.soccerverse.com/graph/subgraphs/id/QmahYLjXXeQajGieM1bqFgtgWCsGfsML8Na2Drratweq5f/graphql. This is the query editor that comes with graph-node. It is only for trying things out: your code should still POST to the endpoint itself.
Previously these were served from graph.soccerverse.com/<name>. That host no longer resolves, so switch old integrations to the endpoints above.
Access and limits
Queries are HTTP POST requests with a JSON body and this header:
Content-Type: application/json
The body is {"query": "..."}, with an optional "variables" object. No key or login is needed.
If a script gets HTTP 403 with the body error code: 1010, set a User-Agent header of your own. Any descriptive string works. Python's default urllib one is blocked, so the Python template below sets its own.
CORS is open (*), so browser apps on any site can call the endpoint. The only request headers it allows are Content-Type and User-Agent.
Limits:
- Rate: 5 requests per second per IP address (bursts of up to 10), then HTTP
429. first: 100 rows if you leave it out, 1 to 1000 if you set it. Anything larger is an error.- Query depth: 16 levels.
- Time: 30 seconds per query.
- Request body: about 1 MB. Larger bodies get HTTP
413. - Read-only: you can query the data but not change it.
Errors come in four kinds:
- GraphQL problems (an unknown field, a
firstthat is too large, an unknown or old deployment ID) come back as HTTP200with anerrorsarray, so always checkerrors. - A body that is not valid JSON, or has no
queryfield, gets HTTP400and a singleerrorstring, not anerrorsarray:{"error":"GraphQL server error (client error): ..."}. - A wrong path answers HTTP
200with{"message":"Not found"}. If you see that, check the URL rules above. - Problems at the gateway use HTTP codes:
403(Cloudflare),413(body too large) and429(rate limit).
Common GraphQL patterns
Minimal curl template
curl -sS 'https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq' \
-H 'Content-Type: application/json' \
--data '{"query":"query { saleClubs(first: 1) { clubId minted remainingInTranche } }"}'
Python standard-library template
import json
import urllib.request
url = "https://play.soccerverse.com/graph/subgraphs/id/QmahYLjXXeQajGieM1bqFgtgWCsGfsML8Na2Drratweq5f"
query = """
query RecentTrades {
trades(first: 3, orderBy: timestamp, orderDirection: desc) {
id
timestamp
asset
amount
sats
price
seller
buyer
}
}
"""
request = urllib.request.Request(
url,
data=json.dumps({"query": query}).encode("utf-8"),
headers={
"Content-Type": "application/json",
"User-Agent": "my-app/1.0", # any descriptive name for your app
},
method="POST",
)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.loads(response.read().decode("utf-8"))
# GraphQL problems still come back as HTTP 200, so check for errors.
if "errors" in payload:
raise RuntimeError(payload["errors"])
print(payload["data"])
Variables
Pass user input as GraphQL variables instead of building query strings. $club: Int! declares a variable, and the variables object supplies its value:
curl -sS 'https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq' \
-H 'Content-Type: application/json' \
--data '{"query":"query($club: Int!) { saleClubs(where: { clubId: $club }) { clubId minted remainingInTranche } }","variables":{"club":50}}'
Finding entities and fields
To see what you can ask for, open the /graphql editor described under Endpoints and use its Docs panel, or read the schema linked in each section below.
Pagination, ordering, and filtering
These subgraphs use The Graph query conventions:
first: Nlimits results: 100 if you leave it out, at most 1000. Lists inside a result (for example thereferralsof a referrer) follow the same rules, so withoutfirstthey stop at 100 items.skip: Npaginates by offset. For large scans, sort by a stable field and page in batches.orderBy: fieldandorderDirection: asc|descsort results. You can sort by a field of a related entity with a double underscore, one level deep, for examplecurrentTotal__referrals.where: { ... }filters results. Filters on a related entity use the underscore form, for exampleprimaryClub_: { clubId: 50 }.- Entity IDs are often bytes/hex strings or stringified numeric IDs depending on the schema. Do not assume all
idfields are decimal.
Example pagination query:
query SaleClubPage {
saleClubs(first: 5, skip: 0, orderBy: clubId, orderDirection: asc) {
clubId
minted
remainingInTranche
trancheIndex
}
}
Democrit/SVC subgraph
Exchange data for the SVC market: completed trades, listed sell orders and their vaults. Listed orders are not always live, so read Democrit pitfalls before you use one.
Endpoint:
https://play.soccerverse.com/graph/subgraphs/id/QmahYLjXXeQajGieM1bqFgtgWCsGfsML8Na2Drratweq5f
Schema: schema.graphql in the public xaya/democrit-evm repository.
Democrit entity table
| Entity | Root fields | Purpose | Important fields |
|---|---|---|---|
Trade | trade, trades | Completed Democrit trades | id, timestamp, asset, amount, sats, price, seller, buyer |
SellOrder | sellOrder, sellOrders | Listed limit sell orders (many are stale, see the pitfall) | id, vault, creator, seller, asset, amount, totalSats, price |
Vault | vault, vaults | The vault behind each listed order | id, founder, asset, balance, sellOrder |
The schema also defines BuyOrder, TradingPool and SellDeposit, but buyOrders, tradingPools and sellDeposits return nothing today (checked 2026-09-29).
Democrit queries
Recent SVC/Democrit trades:
query RecentTrades {
trades(first: 3, orderBy: timestamp, orderDirection: desc) {
id
timestamp
asset
amount
sats
price
seller
buyer
}
}
Response-field notes:
timestampis a Unix timestamp in seconds, returned as a string (GraphQLBigInt).amountis in raw units. Divide by10000to get SVC.satsis WCHI in satoshis. Divide by100000000to get WCHI.priceis sats per raw unit (satsdivided byamount), returned as a string. Divide it by10000to get WCHI per SVC.assetissmc(SVC) on every row today.
Cheapest listed sell orders:
query CheapestSellOrders {
sellOrders(first: 3, orderBy: price, orderDirection: asc) {
id
vault {
id
}
seller
asset
amount
totalSats
price
}
}
Vault sample:
query VaultSample {
vaults(first: 3) {
id
founder
asset
balance
}
}
Check that listed orders still have a vault. Send each order's vault id to get_vaults on the hosted /v1/gsp endpoint, and get two details right:
- Send the ids as numbers:
8033, not"8033". The subgraph returns vault ids as strings, so convert them first, for example withint()in Python orNumber()in JavaScript. The GSP silently skips a string id: there is no error, and that id is simply missing from the reply. - Send
controllerexactly as shown. Any other value, even the same address in capital letters, makes every vault readexists: false.
curl -sS 'https://play.soccerverse.com/v1/gsp' \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"get_vaults","params":{"controller":"Democrit 0xeb265d9fbb05641266366f15ece76a268ef4a2a4","ids":[8033,8250,13608]},"id":1}'
Each row in result.data has "exists": true or "exists": false. Keep only the orders whose vault says "exists": true. The rest are stale and cannot be filled. The reply should have one row per id you sent: if it has fewer, some ids were not numbers.
When this page was checked (2026-09-29), all three vaults in the example said false. The reply format is on the GSP JSON-RPC page. On your own GSP node, send the same request to your node's address.
Democrit pitfalls
- A listed order is not proof that it can be filled. Many
sellOrders(and thevaultsbehind them) are stale. On 2026-09-29 the GSP no longer had the vault for 38 of the 130 listed orders, including 22 of the 40 cheapest. Check them as shown above before you show or use an order. - Seller names are case-sensitive:
sellOrders(where: { seller: "cris86" })matches, but"CRIS86"finds nothing.
Xaya stats subgraph
Public Xaya Accounts contract data on Polygon: names, their owners, moves and profile data. Use it for fast wallet and name lookups.
Endpoint:
https://play.soccerverse.com/graph/subgraphs/id/QmaZXXhj5KpsveiCeQXme19yZParTxRsQzUnbX6ujLg5un
Schema: schema.graphql in the public xaya/stats-subgraph repository.
Xaya entity table
| Entity | Root fields | Purpose | Important fields |
|---|---|---|---|
Address | address, addresses | Wallet/address currently holding or historically related to names | id, names |
Name | name, names | Xaya names, including p/ player names | id, ns, name, owner, and the relations registration, moves, profileData |
Namespace | namespace, namespaces | Xaya namespaces | id, ns, names |
Game | game, games | Game IDs observed in moves | id, game, moves |
Move | move, moves | Raw name moves | id, tx, name, move, payment, games |
GameMove | gameMove, gameMoves | Game-specific move records | id, move, tx, game, gamemove |
Payment | payment, payments | WCHI payment attached to a move | id, move, receiver, amount |
Registration | registration, registrations | Name registration events | id, tx, name |
ProfileData | profileData, profileDatas | Public profile key/value data | account, admin, key, value |
Transaction | transaction, transactions | Chain transaction metadata | id, height, timestamp |
Profile data:
- The key
picb64holds a base64-encoded profile picture, a few kilobytes of text per row, so only ask forvaluewhen you need the picture. - Rows with
admin: truewere set by admin accounts. For example, keypicwith valuehiddenhides a picture. - Filter by
keyto fetch only the rows you need.
Xaya queries
Look up the sample username snailbrain:
query NameByAccount {
names(first: 5, where: { name: "snailbrain" }) {
id
name
ns {
ns
}
owner {
id
}
}
}
Look up the names a wallet owns:
query NamesByOwner {
addresses(where: { id: "0x59f5f46dc284e3414f67eda31ee90126bc48bf95" }) {
id
names(first: 100, orderBy: name, orderDirection: asc) {
id
name
ns {
ns
}
}
}
}
Game moves from the last hour, newest first:
query RecentGameMoves($since: BigInt!) {
gameMoves(
first: 10
orderBy: tx__timestamp
orderDirection: desc
where: { tx_: { timestamp_gt: $since } }
) {
id
game {
game
}
tx {
height
timestamp
}
}
}
Send since in the variables object as a string of Unix seconds. For the last hour, use the current time minus 3600: in a shell, echo $(( $(date +%s) - 3600 )) prints it.
Xaya pitfalls
- Names are case-sensitive.
names(where: { name: "snailbrain" })matches, but"SNAILBRAIN"finds nothing. Names that differ only in case are different names with their own owners, so use the name exactly as it was registered. - Soccerverse player names are in the
pnamespace. A wallet can also own names in other namespaces (for exampleg), so addns_: { ns: "p" }to anamesfilter when you only want players. - Some names are blank, whitespace or only punctuation. Check a name before you show it.
- Always add a time filter when you sort
movesorgameMovesbytx__timestamp, and keep the window short (an hour or a day). With no filter, or one that matches years of history, the query fails with a database error (checked 2026-09-29).
Soccerverse shop/referrals/pricing subgraph
Referral leaderboards, shop pack purchases, club sale status, pricing tiers, pack contents, influence mints, and Uniswap-derived price observations.
The Datacentre REST API route /shop/clubs returns the same club sale data (prices, stock and pack contents, in the same units) as one ready-made row per pack. Use it for a club's current price and stock, and use this subgraph for pack purchases, influence mints and price history.
Endpoint:
https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2Gq
Schema: schema.graphql in the public soccerverse/subgraph repository.
Soccerverse entity table
| Entity | Root fields | Purpose | Important fields |
|---|---|---|---|
Referrer | referrer, referrers | A user that has referred others | account, referrals, bonuses, totals, currentTotal |
Referral | referral, referrals | A referred account | account, referrer, timestamp |
ReferrerBonus | referrerBonus, referrerBonuses | Referral bonus from pack purchase | timestamp, clubId, packsBought, usdSpent, bonusShares |
ReferrerTotal | referrerTotal, referrerTotals | Running referral totals snapshots | timestamp, index, referrals, bonusShares, usdSpent |
SaleTier | saleTier, saleTiers | Shop sale tier / sale contract | name, active, related clubs/pricing/purchases |
PricingStep | pricingStep, pricingSteps | Price tranche data inside a tier | index, numShares, price, fromTotal, toTotal, tier |
SaleClub | saleClub, saleClubs | Club sale state | clubId, tier, pausedInTier, minted, trancheIndex, remainingInTranche |
Pack | pack, packs | Pack for a primary club | primaryClub, shares, maxPacks, cost |
PackShareContent | packShareContent, packShareContents | Clubs/shares contained in a pack | pack, club, num |
PacksBought | packsBought, packsBoughts | Individual pack purchase event | timestamp, height, txHash, buyer, receiver, primaryClub, tier, numPacks, usdSpent, mints |
InfluenceMint | influenceMint, influenceMints | Each club-influence mint from the sale, paid or free, with its price (usdCost) | timestamp, height, club, receiver, num, mintedBefore, tier, usdCost, purchase, referralBonus |
Token | token, tokens | Tracked token metadata | id, decimals, symbol |
TokenPair | tokenPair, tokenPairs | Tracked Uniswap pair | tradedToken, baseToken, uniswapPair, prices |
PriceObservation | priceObservation, priceObservations | Periodic Uniswap moving-average observations | virtualTimestamp, exactTimestamp, cumulativePrice, average24h, pair |
Soccerverse queries
Club sale status for sample club_id=50:
query ClubSaleStatus {
saleClubs(first: 5, where: { clubId: 50 }) {
id
clubId
minted
remainingInTranche
trancheIndex
tier {
name
active
}
pausedInTier {
name
active
}
}
}
Pack composition for sample club_id=50:
query PackForClub50 {
packs(first: 5, where: { primaryClub_: { clubId: 50 } }) {
id
maxPacks
cost
primaryClub {
clubId
minted
remainingInTranche
tier {
name
}
}
shares(first: 20) {
club {
clubId
}
num
}
}
}
Recent pack purchases:
query RecentPackPurchases {
packsBoughts(first: 3, orderBy: timestamp, orderDirection: desc) {
id
timestamp
receiver
numPacks
usdSpent
primaryClub {
clubId
minted
remainingInTranche
}
tier {
name
active
}
}
}
Recent influence mints. A pack buys influence in several clubs, and each club's part is its own mint with its own price:
query RecentInfluenceMints {
influenceMints(first: 3, orderBy: timestamp, orderDirection: desc) {
id
timestamp
club {
clubId
}
receiver
num
usdCost
referralBonus
tier {
name
}
purchase {
id
numPacks
}
}
}
Free mints have purchase: null. They still show a usdCost, which is the price at that time and not money anyone spent. referralBonus: true marks the free bonus given to a referrer.
Current referral leaderboard:
query CurrentReferralLeaders {
referrers(first: 5, orderBy: currentTotal__referrals, orderDirection: desc) {
account
currentTotal {
timestamp
referrals
bonusShares
usdSpent
}
}
}
Use this query for a leaderboard, not referrerTotals. That table gets a new row every time a referrer's totals change, so ordering it directly lists the same account many times.
The price steps of one tier, from step 0 upwards:
query PricingSteps {
pricingSteps(
first: 20
where: { tier_: { name: "tier 1" } }
orderBy: index
orderDirection: asc
) {
index
numShares
price
fromTotal
toTotal
}
}
Price observations:
query PriceObservations {
priceObservations(first: 3, orderBy: exactTimestamp, orderDirection: desc) {
id
virtualTimestamp
exactTimestamp
average24h
pair {
tradedToken {
symbol
decimals
}
baseToken {
symbol
decimals
}
}
}
}
Live results include WCHI/WETH and WETH/USDC pairs.
Soccerverse response-field notes
usdSpent,usdCost,Pack.costandPricingStep.priceare USD amounts with 6 decimals. Divide by 1,000,000: for example,39500000is $39.50.minted,remainingInTranche,numPacks,maxPacks, andnumSharesare plain integer counts.PriceObservation.average24his a large fixed-point integer. Divide it by2**112to get the price of the traded token in units of the base token. Token decimals are already accounted for.trancheIndexandPricingStep.indexcount from 0. The Datacentre/shop/clubsfieldtranchecounts from 1, so a club withtrancheIndex4 showstranche5 there.
Some fields have special values:
- A club is not on sale when
trancheIndexis-1, andremainingInTrancheis then0. ChecktrancheIndex, nottier. Usually such a club is paused, withtier: nullandpausedInTierset. But a club that has minted past the last price step of its tier keeps itstierand has nopausedInTier(club 748 on 2026-09-29). PacksBought.tierisnullfor packs paid with SVV through the voucher contract.PriceObservation.average24hisnullfor a pair's first 24 hours.
Soccerverse pitfalls
- Filters on a related entity are easy to get wrong. For a club's pack, use
packs(where: { primaryClub_: { clubId: 50 } }), notpacks(where: { clubId: 50 }). priceObservationshas many rows per token pair. Sort byexactTimestamp, newest first, to get the latest.
Running your own node
A GSP node you run yourself has no subgraphs (it has no /graph), so keep using the hosted endpoints above. The schemas are public (see the Schema link in each section), so you can also index the same data with your own graph-node.