Passer au contenu principal
Loading market data…
SoccerverseSoccerverse

These developer docs are available in English only. View in English

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.

SubgraphEndpointWhat it holds
Democrit/SVC (democrit-sv)https://play.soccerverse.com/graph/subgraphs/id/QmahYLjXXeQajGieM1bqFgtgWCsGfsML8Na2Drratweq5fThe SVC exchange: trades, listed sell orders and their vaults
Xaya stats (xaya-stats)https://play.soccerverse.com/graph/subgraphs/id/QmaZXXhj5KpsveiCeQXme19yZParTxRsQzUnbX6ujLg5unXaya account names and owners, moves, registrations and profile data
Shop, referrals and pricing (sv-subgraph)https://play.soccerverse.com/graph/subgraphs/id/QmZMK7HjfU35k2iTaWLCp7JNUaVdK5U7Coz8Gcu1xrf2GqReferrals, 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 /graph prefix. Without it you get the website's 404 page.
  • Leave off any trailing slash. A URL ending in / answers {"message":"Not found"}.
  • Use POST. A GET does 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 first that is too large, an unknown or old deployment ID) come back as HTTP 200 with an errors array, so always check errors.
  • A body that is not valid JSON, or has no query field, gets HTTP 400 and a single error string, not an errors array: {"error":"GraphQL server error (client error): ..."}.
  • A wrong path answers HTTP 200 with {"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) and 429 (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: N limits results: 100 if you leave it out, at most 1000. Lists inside a result (for example the referrals of a referrer) follow the same rules, so without first they stop at 100 items.
  • skip: N paginates by offset. For large scans, sort by a stable field and page in batches.
  • orderBy: field and orderDirection: asc|desc sort results. You can sort by a field of a related entity with a double underscore, one level deep, for example currentTotal__referrals.
  • where: { ... } filters results. Filters on a related entity use the underscore form, for example primaryClub_: { clubId: 50 }.
  • Entity IDs are often bytes/hex strings or stringified numeric IDs depending on the schema. Do not assume all id fields 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

EntityRoot fieldsPurposeImportant fields
Tradetrade, tradesCompleted Democrit tradesid, timestamp, asset, amount, sats, price, seller, buyer
SellOrdersellOrder, sellOrdersListed limit sell orders (many are stale, see the pitfall)id, vault, creator, seller, asset, amount, totalSats, price
Vaultvault, vaultsThe vault behind each listed orderid, 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:

  • timestamp is a Unix timestamp in seconds, returned as a string (GraphQL BigInt).
  • amount is in raw units. Divide by 10000 to get SVC.
  • sats is WCHI in satoshis. Divide by 100000000 to get WCHI.
  • price is sats per raw unit (sats divided by amount), returned as a string. Divide it by 10000 to get WCHI per SVC.
  • asset is smc (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 with int() in Python or Number() in JavaScript. The GSP silently skips a string id: there is no error, and that id is simply missing from the reply.
  • Send controller exactly as shown. Any other value, even the same address in capital letters, makes every vault read exists: 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 the vaults behind 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

EntityRoot fieldsPurposeImportant fields
Addressaddress, addressesWallet/address currently holding or historically related to namesid, names
Namename, namesXaya names, including p/ player namesid, ns, name, owner, and the relations registration, moves, profileData
Namespacenamespace, namespacesXaya namespacesid, ns, names
Gamegame, gamesGame IDs observed in movesid, game, moves
Movemove, movesRaw name movesid, tx, name, move, payment, games
GameMovegameMove, gameMovesGame-specific move recordsid, move, tx, game, gamemove
Paymentpayment, paymentsWCHI payment attached to a moveid, move, receiver, amount
Registrationregistration, registrationsName registration eventsid, tx, name
ProfileDataprofileData, profileDatasPublic profile key/value dataaccount, admin, key, value
Transactiontransaction, transactionsChain transaction metadataid, height, timestamp

Profile data:

  • The key picb64 holds a base64-encoded profile picture, a few kilobytes of text per row, so only ask for value when you need the picture.
  • Rows with admin: true were set by admin accounts. For example, key pic with value hidden hides a picture.
  • Filter by key to 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 p namespace. A wallet can also own names in other namespaces (for example g), so add ns_: { ns: "p" } to a names filter 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 moves or gameMoves by tx__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

EntityRoot fieldsPurposeImportant fields
Referrerreferrer, referrersA user that has referred othersaccount, referrals, bonuses, totals, currentTotal
Referralreferral, referralsA referred accountaccount, referrer, timestamp
ReferrerBonusreferrerBonus, referrerBonusesReferral bonus from pack purchasetimestamp, clubId, packsBought, usdSpent, bonusShares
ReferrerTotalreferrerTotal, referrerTotalsRunning referral totals snapshotstimestamp, index, referrals, bonusShares, usdSpent
SaleTiersaleTier, saleTiersShop sale tier / sale contractname, active, related clubs/pricing/purchases
PricingSteppricingStep, pricingStepsPrice tranche data inside a tierindex, numShares, price, fromTotal, toTotal, tier
SaleClubsaleClub, saleClubsClub sale stateclubId, tier, pausedInTier, minted, trancheIndex, remainingInTranche
Packpack, packsPack for a primary clubprimaryClub, shares, maxPacks, cost
PackShareContentpackShareContent, packShareContentsClubs/shares contained in a packpack, club, num
PacksBoughtpacksBought, packsBoughtsIndividual pack purchase eventtimestamp, height, txHash, buyer, receiver, primaryClub, tier, numPacks, usdSpent, mints
InfluenceMintinfluenceMint, influenceMintsEach club-influence mint from the sale, paid or free, with its price (usdCost)timestamp, height, club, receiver, num, mintedBefore, tier, usdCost, purchase, referralBonus
Tokentoken, tokensTracked token metadataid, decimals, symbol
TokenPairtokenPair, tokenPairsTracked Uniswap pairtradedToken, baseToken, uniswapPair, prices
PriceObservationpriceObservation, priceObservationsPeriodic Uniswap moving-average observationsvirtualTimestamp, 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.cost and PricingStep.price are USD amounts with 6 decimals. Divide by 1,000,000: for example, 39500000 is $39.50.
  • minted, remainingInTranche, numPacks, maxPacks, and numShares are plain integer counts.
  • PriceObservation.average24h is a large fixed-point integer. Divide it by 2**112 to get the price of the traded token in units of the base token. Token decimals are already accounted for.
  • trancheIndex and PricingStep.index count from 0. The Datacentre /shop/clubs field tranche counts from 1, so a club with trancheIndex 4 shows tranche 5 there.

Some fields have special values:

  • A club is not on sale when trancheIndex is -1, and remainingInTranche is then 0. Check trancheIndex, not tier. Usually such a club is paused, with tier: null and pausedInTier set. But a club that has minted past the last price step of its tier keeps its tier and has no pausedInTier (club 748 on 2026-09-29).
  • PacksBought.tier is null for packs paid with SVV through the voucher contract.
  • PriceObservation.average24h is null for 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 } }), not packs(where: { clubId: 50 }).
  • priceObservations has many rows per token pair. Sort by exactTimestamp, 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.

Nos Partenaires