At Sorare, we are committed to providing an open platform for developers to build upon.
While our Cards are stored on the Ethereum blockchain (or within a Starkware rollup) we support an API that provides more detailed information.
The Sorare API are provided by GraphQL. The API is hosted on https://api.sorare.com/graphql. The documentation can be found under the Docs section of the GraphQL playground.
You can easily download the GraphQL schema using the following HTTP endpoint:
$ curl -o schema.graphql https://api.sorare.com/graphql/schemaThe MLB-specific and NBA-specific resources are prefixed with Baseball and NBA respectively: BaseballCard, BaseballPlayer, BaseballFixture, BaseballLeaderboard, etc.
To authenticate yourself programmatically through our GraphQL API you'll need:
- your email
- the hashed version of your password
Your password needs to be hashed client-side using a salt. The salt can be retrieved with a HTTP GET request against our https://api.sorare.com/api/v1/users/<youremail> endpoint:
Example:
$ curl https://api.sorare.com/api/v1/users/myemail@mydomain.com
{"salt":"$2a$11$SSOPxn8VSUP90llNuVn.nO"}The hashed password must be computed with bcrypt:
Example in JavaScript:
import bcrypt from 'bcryptjs';
const hashedPassword = bcrypt.hashSync(password, salt);Example in Ruby:
require "bcrypt"
hashed_password = BCrypt::Engine.hash_secret(password, salt)Example in Python:
import bcrypt
hashed_password = bcrypt.hashpw(password, salt)Please also make sure to set the content-type HTTP header to application/json.
For short and long-lived authentication, you should request a JWT token.
We provide JWT tokens within the signIn mutation. They can be retrieved using the following mutation:
mutation SignInMutation($input: signInInput!) {
signIn(input: $input) {
currentUser {
slug
}
jwtToken(aud: "<YourAud>") {
token
expiredAt
}
errors {
message
}
}
}It expects the following variables:
{
"input": {
"email": "your-email",
"password": "your-hashed-password"
}
}<YourAud> is a mandatory string parameter that identifies the recipients that the JWT is intended for. You can read more about "aud" (Audience) here. We recommend to use an aud reflecting the name of your app - like myappname - to make it easier to debug & track.
$ curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-d '{
"operationName": "SignInMutation",
"variables": { "input": { "email": "<YourEmail>", "password": "<YourHashPassword>" } },
"query": "mutation SignInMutation($input: signInInput!) { signIn(input: $input) { currentUser { slug } jwtToken(aud: \"<YourAud>\") { token expiredAt } errors { message } } }"
}'
{"data":{"signIn":{"currentUser":{"slug":"<YourSlug>"},"jwtToken":{"token":"<YourJWTToken>","expiredAt":"..."},"errors":[]}}}You shall then pass the token with an Authorization header alongside a JWT-AUD header to all next API requests:
$ curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-H 'Authorization: Bearer <YourJWTToken>' \
-H 'JWT-AUD: <YourAud>' \
-d '{
"operationName": "CurrentUserQuery",
"query": "query CurrentUserQuery { currentUser { slug email } }"
}'
{"data":{"currentUser":{"slug":"<YourSlug>","email":"<YourEmail>"}}}The token will expire after 30 days.
Please refer to the errors field to understand why a signIn mutation failed.
If currentUser is null and you don't have any errors, it's because the user has 2FA setup. Please follow the next section to handle 2FA signings.
Please note also that if the token has been issued from a specific IP address and you try to generate it from another one, 2FA will automatically activate and you will need the code sent to your email to complete authentication.
For account with 2FA enabled the signIn mutation will set the otpSessionChallenge field instead of the currentUser one.
mutation SignInMutation($input: signInInput!) {
signIn(input: $input) {
currentUser {
slug
}
jwtToken(aud: "<YourAud>") {
token
expiredAt
}
otpSessionChallenge
errors {
message
}
}
}Example:
$ curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-d '{
"operationName": "SignInMutation",
"variables": { "input": { "email": "<YourEmail>", "password": "<YourHashPassword>" } },
"query": "mutation SignInMutation($input: signInInput!) { signIn(input: $input) { currentUser { slug } jwtToken(aud: \"<YourAud>\") { token expiredAt } otpSessionChallenge errors { message } } }"
}'
{"data":{"signIn":{"currentUser":null,"jwtToken":null,"otpSessionChallenge":"3a390a0661cd6f4944205f68c13fd04f","errors":[]}}}In this case, you will need to make another call to the signIn mutation and provide the otpSessionChallenge value you received and a one-time token from your 2FA device as otpAttempt:
{
"input": {
"otpSessionChallenge": "eca010be19a80de5c134c324af24c36f",
"otpAttempt": "788143"
}
}Example:
$ curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-d '{
"operationName": "SignInMutation",
"variables": { "input": { "otpSessionChallenge": "<YourOTPSessionChallenge>", "otpAttempt": "<YourOTPAttempt>" } },
"query": "mutation SignInMutation($input: signInInput!) { signIn(input: $input) { currentUser { slug } jwtToken(aud: \"<YourAud>\") { token expiredAt } errors { message } } }"
}'
{"data":{"signIn":{"currentUser":{"slug":"<YourSlug>"},"jwtToken":{"token":"<YourJWTToken>","expiredAt":"..."},"errors":[]}}}There is no way currently to revoke the token.
Should the Terms & Conditions of Sorare get updated, you might need to accept them before being able to sign in. Please refer to https://sorare.com/terms_and_conditions to read the latest version of Sorare's terms.
You can accept the terms without being signed in by retrieving the tcuToken returned by the failing signIn mutation with must_accept_tcus error:
mutation SignInMutation($input: signInInput!) {
signIn(input: $input) {
currentUser {
slug
}
jwtToken(aud: "<YourAud>") {
token
expiredAt
}
otpSessionChallenge
tcuToken
errors {
message
}
}
}If the tcuToken is set, you can accept the updated Terms & Conditions with the following mutation:
mutation AcceptTermsMutation($input: acceptTermsInput!) {
acceptTerms(input: $input) {
errors {
message
}
}
}And the following variables:
{
"input": {
"acceptTerms": true,
"acceptPrivacyPolicy": true,
"acceptGameRules": true,
"tcuToken": "<YourTcuToken>"
}
}Once terms are accepted, you will be able to sign in again.
With our OAuth API, users can sign-in to your service using their Sorare account, which allows you to request data on their behalf.
In order to use our OAuth API, you need a Client ID and Secret for your application. You can create these yourself, self-service, at sorare.com/settings/developer.
Sorare currently supports only the following OAuth 2.0 grant flows:
- Authorization Code
- Client Credentials
Once you create your application, you will be provided with:
- OAuth Client ID
- OAuth Secret (keep this secret!)
All OAuth applications are provided with one scope which allows access to the following:
- Basic user information, including their nickname, avatar, and wallet address
- User's cards, achievements and favorites
- User's auctions, offers and notifications
The following are not accessible:
- Email addresses
- Future lineups and rewards
- Claiming rewards
- Bidding, selling, or making offers cards
- Accepting offers or initiating withdrawals
First you need to create a "Login with Sorare" link in your app and use the following href:
https://sorare.com/oauth/authorize?client_id=<YourUID>&redirect_uri=<YourURLEncodedCallbackURI>&response_type=code&scope=
Once signed in to Sorare, the user will be asked to authorize your app and will ultimately be redirected to your redirect_uri with a ?code= query parameter, for instance https://myapp.com/auth/sorare/callback?code=<YourCode>.
To request an OAuth access token you can then call the https://api.sorare.com/oauth/token endpoint with the following parameters:
client_id=<YourOAuthUID>client_secret=<YourOAuthSecret>code=<TheRetrievedCode>grant_type=authorization_coderedirect_uri=<TheSameCallbackURIAsBefore>
To refresh an OAuth token you can then call the https://api.sorare.com/oauth/token endpoint with the following parameters:
client_id=<YourOAuthUID>client_secret=<YourOAuthSecret>refresh_token=<RefreshToken>grant_type=refresh_token
Example:
$ curl -X POST "https://api.sorare.com/oauth/token" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=<YourOAuthUID>&client_secret=<YourOAuthSecret>&code=<TheRetrievedCode>&grant_type=authorization_code&redirect_uri=<TheSameCallbackURIAsBefore>'
{"access_token":"....", "refresh_token": "....", "token_type":"Bearer","expires_in":7200,"scope":"public","created_at":1639608238}You can then use the access_token the same way you would use a JWT token:
curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-H 'Authorization: Bearer <TheUserAccessToken>' \
-d '{
"operationName": "CurrentUserQuery",
"query": "query CurrentUserQuery { currentUser { slug } }"
}'
{"data":{"currentUser":{"slug":"<ASlug>"}}}You can refresh the token
$ curl -X POST "https://api.sorare.com/oauth/token" \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'client_id=<YourOAuthUID>&client_secret=<YourOAuthSecret>&refresh_token=<RefreshToken>&grant_type=refresh_token'
{"access_token":"....", "refresh_token": "....", "token_type":"Bearer","expires_in":7200,"refresh_token":"...","scope":"public","created_at":1639608239}You can revoke the token
$ curl -X POST "https://api.sorare.com/oauth/revoke" \
-H 'content-type: application/x-www-form-urlencoded' \
-d "client_id=<YourOAuthUID>&client_secret=<YourOAuthSecret>&token=<TheUserAccessToken>"The GraphQL API is rate limited. You can generate an extra API Key yourself, self-service, at sorare.com/settings/developer to raise those limits.
Here are the configured limits:
- Unauthenticated API calls: 20 calls per minute
- Authenticated (JWT or OAuth) API calls: 60 calls per minute
- API Key API calls: 200 calls per minute
- 40 inflight queries. This only takes into account the backend/server processing part and excludes any network delays.
The API key should be passed in an http APIKEY header.
Example:
curl 'https://api.sorare.com/graphql' \
-H 'content-type: application/json' \
-H 'APIKEY: <YourPrivateAPIKey>' \
-H 'Authorization: Bearer <TheUserAccessToken>' \
-d '{
"operationName": "CurrentUserQuery",
"query": "query CurrentUserQuery { currentUser { slug } }"
}'Whenever you perform too many requests, the GraphQL API will answer with a 429 HTTP error code and add a Retry-After: <TimeToWaitInSeconds> header (see RFC) to the response so your code can rely on it to understand how long it should wait before retrying.
The GraphQL API enforces a maximum payload size for all incoming requests (query, variables and extensions). Requests exceeding this limit will be rejected with a 413 Request Entity Too Large HTTP error.
To stay within the limit, we recommend:
- Keeping query selections minimal — only request the fields you need.
- Avoiding overly large inline arguments or input objects.
- Splitting large mutations into smaller, batched requests.
The GraphQL queries have complexity and depth limits. An API Key created from your developer settings raises those limits.
- Depth reflects the longest nested fields chain.
- Complexity reflects the potential total number of fields that would be returned. If the query asks for the first 50 cards, the complexity is computed on 50 cards, even if the result set is composed of 1 card.
We have the following limits:
| Depth limit | Complexity limit | |
|---|---|---|
| Anonymous API calls | 7 | 500 |
| Anonymous subscription | 7 | 500 |
| Authenticated API calls | 12 | 30 000 |
| Authenticated subscription | 9 | 1 500 |
Our GraphQL API cannot be called from the browser on another domain than the ones we support. Therefore, it's expected to get a Blocked by CORS policy [...]: The ‘Access-Control-Allow-Origin’ header has a value [...] error.
Please consider calling the API from your backend servers.
A common use case in GraphQL is traversing the relationship between sets of objects. There are a number of different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer.
Read more about GraphQL pagination on their official documentation.
At Sorare, we use both plural types for connections with a limited cardinality and cursor-based pagination for the others.
A working JavaScript code sample demonstrating how to leverage the cursor to iterate on all cards of a single user is available in examples/allCardsFromUser.js.
Every operation that involves card or money transfer must be signed with your Starkware private key. It can be exported from sorare.com using your wallet.
Make sure to keep your Private Key secret.
To sign with your Starkware private key in JavaScript, we recommend using the JavaScript package @sorare/crypto.
The prepareBid, prepareOffer and prepareAcceptOffer mutations return different types of AuthorizationRequest depending on the asset and on the payment rail. A card that lives on Solana produces a Solana request; a payment in SOL produces another; a payment in ETH on Base produces another still. You must branch on the __typename of each returned request and build the matching approval — there is no single signing routine that covers them all.
AuthorizationRequest type |
Used for | Example | Approval field |
|---|---|---|---|
StarkexTransferAuthorizationRequest |
Legacy StarkEx card & fund transfers | authorizations.js | starkexTransferApproval |
StarkexLimitOrderAuthorizationRequest |
Legacy StarkEx limit orders | authorizations.js | starkexLimitOrderApproval |
MangopayWalletTransferAuthorizationRequest |
Fiat wallet payments | authorizations.js | mangopayWalletTransferApproval |
SolanaTokenTransferAuthorizationRequest |
Card (NFT) transfer on Solana | solanaTokenTransfer.js | solanaTokenTransferApproval |
SolanaBankTransferAuthorizationRequest |
Payment in SOL | solanaBankTransfer.js | solanaBankTransferApproval |
EthereumBankTransferAuthorizationRequest |
Payment in ETH on Base | baseBankTransfer.js | ethereumBankTransferApproval |
Every approval is submitted alongside the fingerprint of the request it answers.
Note that @sorare/crypto supports StarkEx only — it contains a single StarkEx signature implementation and does not help for Solana or Base. buildApprovals in authorizations.js is likewise StarkEx-only. For Solana requests, sign with @solana/kit as shown in the examples above; for Base requests, sign with viem.
Player Cards are minted on Solana as Metaplex Bubblegum v2 compressed NFTs and are moved by Sorare's Transfer Proxy program. See web3/README.md for the programs, contracts and collections involved.
Solana requests are not signed with your Starkware private key. They are signed with your Solana key pair, which is derived from the Sorare (Ethereum) private key you export from your wallet:
- SLIP-0010 HD derivation, using the Ethereum private key bytes as the master seed
- derivation path
m/44'/501'/0'/0'(the standard Solana path) - an ed25519 key pair built from the derived private key bytes
A working JavaScript code sample is available in examples/solanaKeyPair.js.
The address of the derived key pair is the senderAddress of the authorization request. Checking the derived address against senderAddress is the fastest way to confirm your derivation before you start debugging signatures.
Once you hold the key pair, signing a SolanaTokenTransferAuthorizationRequest means building this exact message from the request:
const message = [
'TRANSFER',
transferProxyProgramAddress,
merkleTreeAddress,
leafIndex.toString(),
nonce,
expirationTimestamp.toString(),
receiverAddress,
'0x',
originator,
].join(':');then UTF-8 encoding it, hashing it with SHA-256, signing the resulting 32-byte hash with ed25519, and Base58-encoding the signature.
Three things are easy to get wrong here, and each of them produces a well-formed signature that is silently and always rejected:
- you sign the SHA-256 hash, not the message string
assetIdis not part of the signed message — the card is identified on chain bymerkleTreeAddressandleafIndex— even thoughassetIdis returned in the requestsenderAddressis not part of the signed message either, since it is implied by the signing key, whiletransferProxyProgramAddressandoriginatorare
The '0x' entry is a literal empty data field, not a placeholder to substitute.
The resulting approval has exactly three fields. nonce and expirationTimestamp are echoed back unchanged from the request, because both are part of the signed message:
const approval = {
fingerprint: solanaTokenTransferAuthorizationRequest.fingerprint,
solanaTokenTransferApproval: {
signature, // Base58 string
nonce, // String holding a uint32
expirationTimestamp, // Int, unix seconds
},
};A working JavaScript code sample is available in examples/solanaTokenTransfer.js.
To list the latest auctions, you can use the following query:
query ListLast10EnglishAuctions {
tokens {
liveAuctions(last: 10) {
nodes {
id
currentPrice
endDate
bestBid {
amounts { wei }
bidder {
... on User {
nickname
}
}
}
minNextBid
anyCards {
slug
name
rarityTyped
}
}
}
}
}A working JavaScript code sample is available in examples/listEnglishAuctions.js.
The GraphQL API needs to be called authenticated (see above how to get an Authorization token)
To make a bid on an auction, you need:
- your Starkware private key
- the
idof the auction you want to bid for - the
amountyou want to bid - the
exchangeRateIdspecifying the exchange rate to use to bid
Here are the steps required to bid:
- Retrieve the current
exchangeRatedIdusing theconfigquery:
query ConfigQuery {
config {
exchangeRate {
id
}
}
}- Get the list of
AuthorizationRequestobjects from theprepareBidmutation on the auction you want to bid for, with the amount you want to bid:
const prepareBidInput = {
auctionId: 'EnglishAuction:b50f54a7-752a-4890-ac62-75ee4be78b33',
amount: '1000000000000000000',
settlementInfo: {
currency: 'WEI',
paymentMethod: 'WALLET',
exchangeRateId: 'ExchangeRate:a8c74db9-b112-46cf-9c40-6f4ded6c2bb0',
},
};mutation PrepareBid($input: prepareBidInput!) {
prepareBid(input: $input) {
authorizations {
fingerprint
request {
...AuthorizationRequestFragment
}
}
}
}
${authorizationRequestFragment}AuthorizationRequestFragment is defined in authorizations.js.
- Sign all
AuthorizationRequestobjects and build thebidInputargument.buildApprovalsis defined in authorizations.js.
prepareBid returns different types of AuthorizationRequest depending on the payment rail, and buildApprovals only handles the StarkEx and Mangopay ones. Branch on the __typename of each request and see Authorization request types for the full list — in particular solanaBankTransfer.js if you pay in SOL, and baseBankTransfer.js if you pay in ETH on Base.
const approvals = buildApprovals(starkPrivateKey, authorizations);
const bidInput = {
approvals,
auctionId: 'EnglishAuction:b50f54a7-752a-4890-ac62-75ee4be78b33',
amount: '1000000000000000000',
settlementInfo: {
currency: 'WEI',
paymentMethod: 'WALLET',
exchangeRateId: 'ExchangeRate:a8c74db9-b112-46cf-9c40-6f4ded6c2bb0',
},
clientMutationId: crypto.randomBytes(8).join(''),
};Note that the clientMutationId is using a random ID.
- Call the
bidmutation:
mutation Bid($input: bidInput!) {
tokenBid(input: $input) {
bid {
id
}
errors {
message
}
}
}A JavaScript code sample is available in examples/bidAuctionWithEth.js.
The GraphQL API needs to be called authenticated (see above how to get an Authorization token)
To create a Direct or Single Sale offer, you need:
- your Starkware private key
- the
assetIdof the card(s) you want to send and/or receive - the amount you want to receive or send
- the
slugof the user you want to make a private offer (Direct Offer only)
Here are the steps required to create an offer:
- Build the
prepareOfferInputargument:
const prepareOfferInput = {
type: 'SINGLE_SALE_OFFER',
sendAssetIds: [
'0x04002c8934c7fadd5a832a693b8a9d295a915fb1d0c2250d824ae18e7c5bba7a',
],
receiveAssetIds: [],
receiveAmount: {
amount: '1000000000000000000', // 1 eth
currency: 'WEI',
},
clientMutationId: crypto.randomBytes(8).join(''),
};
const prepareOfferInput = {
type: 'DIRECT_OFFER',
sendAssetIds: [
'0x04002c8934c7fadd5a832a693b8a9d295a915fb1d0c2250d824ae18e7c5bba7a',
],
receiveAssetIds: [
'x04003b0dbdf7d5d8037fdf34f0dac9f3a400eddd67df72fff46474fb6b39bb43',
],
sendAmount: {
amount: '1000000000000000000', // 1 eth
currency: 'WEI',
},
receiverSlug: 'some-user-slug',
clientMutationId: crypto.randomBytes(8).join(''),
};- Get the list of
AuthorizationRequestobjects from theprepareOffermutation:
mutation PrepareOffer($input: prepareOfferInput!) {
prepareOffer(input: $input) {
authorizations {
...AuthorizationRequestFragment
}
errors {
message
}
}
}
${authorizationRequestFragment}AuthorizationRequestFragment is defined in authorizations.js.
- Sign all
AuthorizationRequestobjects and build thecreateSingleSaleOfferInputorcreateDirectOfferInputargument.buildApprovalsis defined in authorizations.js.
prepareOffer returns different types of AuthorizationRequest depending on where the card lives and on the payment rail, and buildApprovals only handles the StarkEx and Mangopay ones. Branch on the __typename of each request and see Authorization request types for the full list. If the card you are sending is on Solana — which is the case for both SINGLE_SALE_OFFER and DIRECT_OFFER on migrated cards — you will get a SolanaTokenTransferAuthorizationRequest, signed as described in Signing Solana authorization requests and shown in solanaTokenTransfer.js. Payments produce a SolanaBankTransferAuthorizationRequest (solanaBankTransfer.js) or an EthereumBankTransferAuthorizationRequest (baseBankTransfer.js).
const approvals = buildApprovals(starkPrivateKey, authorizations);
const createSingleSaleOfferInput = {
approvals,
dealId: crypto.randomBytes(8).join(''),
assetId: '0x04002c8934c7fadd5a832a693b8a9d295a915fb1d0c2250d824ae18e7c5bba7a',
receiveAmount: {
amount: '1000', // 10.00 euros
currency: 'EUR',
},
clientMutationId: crypto.randomBytes(8).join(''),
};
const createDirectOfferInput = {
approvals,
dealId: crypto.randomBytes(8).join(''),
sendAssetIds: [
'0x04002c8934c7fadd5a832a693b8a9d295a915fb1d0c2250d824ae18e7c5bba7a',
],
receiveAssetIds: [
'x04003b0dbdf7d5d8037fdf34f0dac9f3a400eddd67df72fff46474fb6b39bb43',
],
sendAmount: {
amount: '1000', // 10.00 euros
currency: 'EUR',
},
receiverSlug: 'some-user-slug',
clientMutationId: crypto.randomBytes(8).join(''),
};Note that the clientMutationId and dealId are using random IDs.
- Call the
createSingleSaleOffer(orcreateDirectOffer) mutation:
mutation CreateSingleSaleOffer($input: createSingleSaleOfferInput!) {
createSingleSaleOffer(input: $input) {
tokenOffer {
id
}
errors {
message
}
}
}
mutation CreateDirectOffer($input: createDirectOfferInput!) {
createDirectOffer(input: $input) {
tokenOffer {
id
}
errors {
message
}
}
}A working JavaScript code sample is available in examples/createSingleSaleOffer.js.
The GraphQL API needs to be called authenticated (see above how to get an Authorization token)
To accept a Direct or Single Sale offer, you need:
- your Starkware private key
- the
idof the offer you want to accept - the
exchangeRateIdspecifying the exchange rate to use when accepting the offer
Here are the steps required to accept an offer:
- Retrieve the current
exchangeRatedIdusing theconfigquery:
query ConfigQuery {
config {
exchangeRate {
id
}
}
}- Build the
prepareAcceptOfferInputargument:
const prepareAcceptOfferInput = {
offerId: 'SingleSaleOffer:df241f08-5dee-4cc3-a8f3-b891c9e68c7f',
settlementInfo: {
currency: 'WEI',
paymentMethod: 'WALLET',
exchangeRateId: 'ExchangeRate:a8c74db9-b112-46cf-9c40-6f4ded6c2bb0',
},
};- Get the list of
AuthorizationRequestobjects from theprepareAcceptOffermutation:
mutation PrepareAcceptOffer($input: prepareAcceptOfferInput!) {
prepareAcceptOffer(input: $input) {
authorizations {
...AuthorizationRequestFragment
}
errors {
message
}
}
}
${authorizationRequestFragment}AuthorizationRequestFragment is defined in authorizations.js.
- Sign all
AuthorizationRequestobjects and build theacceptOfferInputargument.buildApprovalsis defined in authorizations.js.
prepareAcceptOffer returns different types of AuthorizationRequest depending on where the card lives and on the payment rail, and buildApprovals only handles the StarkEx and Mangopay ones. Branch on the __typename of each request and see Authorization request types for the full list. Accepting a direct offer where you send a card on Solana yields a SolanaTokenTransferAuthorizationRequest (solanaTokenTransfer.js, and Signing Solana authorization requests); paying yields a SolanaBankTransferAuthorizationRequest (solanaBankTransfer.js) or an EthereumBankTransferAuthorizationRequest (baseBankTransfer.js).
const approvals = buildApprovals(starkPrivateKey, authorizations);
const acceptOfferInput = {
approvals,
offerId: 'SingleSaleOffer:df241f08-5dee-4cc3-a8f3-b891c9e68c7f',
settlementInfo: {
currency: 'WEI',
paymentMethod: 'WALLET',
exchangeRateId: 'ExchangeRate:a8c74db9-b112-46cf-9c40-6f4ded6c2bb0',
},
clientMutationId: crypto.randomBytes(8).join(''),
};Note that the clientMutationId is using a random ID.
- Call the
acceptOffermutation:
mutation AcceptSingleSaleOffer($input: acceptOfferInput!) {
acceptOffer(input: $input) {
tokenOffer {
id
}
errors {
message
}
}
}A working JavaScript code sample is available in examples/acceptSingleSaleOffer.js.
const slugs = [slug1, slug2];query GetBaseballCardBySlugs($slugs: [String!]) {
cards(slugs: $slugs) {
assetId
slug
rarityTyped
season
serialNumber
positions
team {
name
}
anyPlayer {
displayName
}
}
}A working JavaScript code sample is available in examples/getBaseballCard.js.
const slugs = [slug1, slug2];query GetNBACardsPrices($slugs: [String!]!) {
cards(slugs: $slugs)
latestEnglishAuction {
bestBid {
amount
amountInFiat { eur gbp usd }
}
}
}
}A working JavaScript code sample is available in examples/getNBACardPrice.js.
The Sorare API provides different GraphQL events to subscribe to:
anyCardWasUpdated: triggers every time a card is updated. This can be filtered using the following arguments:ages,cardEditions,playerSlugs,positions,owned,rarities,seasonStartYears,serialNumbers,shirtNumbers,slugs,sportcurrentUserWasUpdated: scoped to the current user, triggers every time the current user is updated (only works when authenticated)gameWasUpdated: triggers every time a game is updatedtokenAuctionWasUpdated: triggers every time an auction is updated (football,baseball&nbacollections)tokenOfferWasUpdated: triggers every time an offer is updated (football,baseball&nbacollections)
The websocket URL to use is wss://ws.sorare.com/cable.
Sorare's GraphQL subscriptions are implemented through websockets with the actioncable-v1-json sub-protocol. Sorare relies on ActionCable because the sorare.com website has been scaled on a Ruby on Rails stack.
In order to ease the websocket + actioncable-v1-json sub-protocoal usage outside of a Ruby on Rails environment, you can use the TypeScript/JavaScript package @sorare/actioncable:
$ yarn add @sorare/actioncableconst { ActionCable } = require('@sorare/actioncable');
const cable = new ActionCable({
headers: {
// 'Authorization': `Bearer <YourJWTorOAuthToken>`,
// 'APIKEY': '<YourOptionalAPIKey>'
},
});
cable.subscribe('anyCardWasUpdated { slug }', {
connected() {
console.log('connected');
},
disconnected(error) {
console.log('disconnected', error);
},
rejected(error) {
console.log('rejected', error);
},
received(data) {
const anyCardWasUpdated = data?.result?.data?.anyCardWasUpdated;
if (!anyCardWasUpdated) {
return;
}
const { id } = anyCardWasUpdated;
console.log('a card was updated', id);
},
});A working JavaScript code sample is available in examples/subscribeAllCardUpdates.js.
The below example will start a subscription for updates on the current user and exit on the first of the following events:
- Connection failures or error
- An update is received for the current user
const { ActionCable } = require("@sorare/actioncable");
const cable = new ActionCable({
headers: {
// 'Authorization': `Bearer <YourJWTorOAuthToken>`,
// 'APIKEY': '<YourOptionalAPIKey>'
},
});
cable.subscribe('currentUserWasUpdated { slug nickname }', {
connected() {
console.log("connected");
},
disconnected(error) {
console.log("disconnected", error);
process.exit(1);
},
rejected(error) {
console.log("rejected", error);
process.exit(1);
},
received(data) {
if (data?.result?.errors?.length > 0) {
console.log('error', data?.result?.errors);
process.exit(1);
return;
}
const currentUserWasUpdated = data?.result?.data?.currentUserWasUpdated;
if (!currentUserWasUpdated) {
return;
}
const { slug } = currentUserWasUpdated;
console.log('current user was updated', slug);
process.exit(0);
}
});This example can be found in examples/subcribeCurrentUserUpdates.js which can be run with the following environment parameters:
JWT_TOKENJWT_AUD
Example of GraphQL subscription to get notified each time an offer is updated:
subscription {
tokenOfferWasUpdated {
status
actualReceiver {
... on User {
slug
}
}
sender {
... on User {
slug
}
}
senderSide {
amounts {
wei
eur
usd
gbp
}
anyCards {
assetId
slug
collection
}
}
receiverSide {
amounts {
wei
eur
usd
gbp
}
anyCards {
assetId
slug
collection
}
}
}
}Example of GraphQL subscription to get notified each time an auction is updated:
subscription {
tokenAuctionWasUpdated {
open
bestBid {
amounts {
wei
eur
usd
gbp
}
bidder {
... on User {
slug
}
}
}
bids {
nodes {
amounts {
wei
eur
usd
gbp
}
bidder {
... on User {
slug
}
}
}
}
anyCards {
assetId
slug
collection
}
}
}A working JavaScript code sample is available in examples/subscribeTokenWasUpdated.js.
$ pip3 install websocket-clientimport websocket
import json
import time
w_socket = 'wss://ws.sorare.com/cable'
identifier = json.dumps({"channel": "GraphqlChannel"})
subscription_query = {
"query": "subscription onAnyCardUpdated { aCardWasUpdated { slug } }",
"variables": {},
"operationName": "onAnyCardUpdated",
"action": "execute"
}
def on_open(ws):
subscribe_command = {"command": "subscribe", "identifier": identifier}
ws.send(json.dumps(subscribe_command).encode())
time.sleep(1)
message_command = {
"command": "message",
"identifier": identifier,
"data": json.dumps(subscription_query)
}
ws.send(json.dumps(message_command).encode())
def on_message(ws, data):
message = json.loads(data)
type = message.get('type')
if type == 'welcome':
pass
elif type == 'ping':
pass
elif message.get('message') is not None:
print(message['message'])
def on_error(ws, error):
print('Error:', error)
def on_close(ws, close_status_code, close_message):
print('WebSocket Closed:', close_message, close_status_code)
def long_connection():
ws = websocket.WebSocketApp(
w_socket,
on_message=on_message,
on_close=on_close,
on_error=on_error,
on_open=on_open
)
ws.run_forever()
if __name__ == '__main__':
long_connection()A working Python3 code sample is available in examples/subscribe_all_card_updates.py.
Using the gqlactioncable package, it is now possible to make subscriptions using graphql-python/gql.
$ pip install gqlactioncableimport asyncio
from gql import Client, gql
from gqlactioncable import ActionCableWebsocketsTransport
async def main():
transport = ActionCableWebsocketsTransport(
url="wss://ws.sorare.com/cable",
keep_alive_timeout=60,
)
async with Client(transport=transport) as session:
subscription = gql(
"""
subscription onAnyCardUpdated {
anyCardWasUpdated {
card {
name
grade
}
}
}
"""
)
async for result in session.subscribe(subscription):
print(result["anyCardWasUpdated"])
asyncio.run(main())This example is available in examples/gql_subscription_all_cards.py.
See also an example for http queries with gql: examples/gql_query_all_cards.py.
Note: the backend might stop sending data after some time. See this comment to implement a reconnecting session.

