The STX exchange API supports GraphQL to query data and execute actions and secure web sockets to push data to the user. For example, when placing an order the user will call a GraphQL mutation and be informed of the status. Later, when trades are executed, the user will be notified of the trades on a socket if they are listening to the socket.
To many REST engineers the concepts of GraphQL might be new. Essentially GraphQL allows us to create a configurable REST endpoint where the caller can decide what data they get, specify search parameters and so on, all in a single endpoint. If you are new to GraphQL I would suggest you take the time to learn the technology using the great tutorials on Graphql.org.
To experiment with GraphQL queries, the user should use the STX GraphiQL Console that is available on our pre-production server. This web-based console contains the detailed documentation of the various mutations that we will cover in this document. It is important that the user know the basics of GraphQL queries as it will make reading the rest of the document much easier. It also provides a method to prototype the queries that the user wishes to run. Please contact STX to get access to the console.
To work with STX web sockets, it is highly advisable that the developer look at the Phoenix Socket Library in order to make the integration process easier. The developer can, of course, connect to the sockets directly but that will require the user to parse the messages themselves. There are a number of Phoenix Socket Libraries for different languages such as Java, C# and Python either supported by the phoenix project or available as open source.
We have a postman collections with sample graplql queries and socket examples.
In addition to the GraphQL and WebSocket APIs, STX supports a limited implementation of the FIX trading protocol. The implementation is currently undergoing development. If you are interested in integrating with this API please let us know.
It is also worth noting that the API implemented in the exchange server is designed to service our client apps and may not contain some aspects that clients may want to do their own integrations. Please contact support and request anything that you might need to accomplish your integration. We have already added many features for existing market makers and want to assist your integration with our exchange.
The username and the password you create as part of the registration is your API credentials too.
To create credentials in our pre-production server, download the STX Beta app on iOS device using the TestFlight link and follow the instructions.
To create credentials in our Ontario production server, download the STX app on iOS device from the App Store using this link App Store and register. Please note that the app is available only in Ontario region.
Production STX GraphiQL Console
Although it is possible to register users via our API directly it is not encouraged. STX encourages all users to register by using the STX application on their mobile device. The reason why we encourage this is because there are a lot of disclosures, terms and conditions and other aspects defined by law that have to be adhered to. The STX development and compliance teams spent a significant amount of resources getting all this right. For the purposes of this wiki, we will not document the registration APIs. If you really need to use them please contact STX directly.
The STX server uses standard JSON Web Token authorization with Authorization headers. The token must be set as a header on every endpoint that requires authorization and must be set in GraphQL and Web Socket authorization mechanisms. The token is fetched by calling the login mutation on the server with the proper credentials. The structure of the login credentials is shown here.
"Credentials used to login to the system."
input LoginCredentials {
"The email of the user."
email: String!
"The password for the given email username."
password: String!
"The information about the device that the user is logging in on."
deviceInfo:DeviceInfo
}
Note that these credentials assume that the user has already registered an account on the system and validated the email of the account. Contact a STX representative to obtain staging system credentials.
The registration process is best done through our client app currently available on iOS which you can find by scanning the QR code to the left.
Once registered and approved, logging in to the platform to obtain a token to use for authenticated calls requires a user to call the login mutation as defined here:
"Contains information on a device registered in the system."
input DeviceInfo {
"The unique id of the device from the device itself."
deviceId: String
}
"Credentials used to login to the system."
input LoginCredentials {
"The email of the user."
email: String!
"The password for the given email username."
password: String!
"The information about the device that the user is logging in on."
deviceInfo: DeviceInfo
}
type RootMutationType {
""" Login to the platform using the credentials. Please note that if the user has 2FA enabled the actual login will occur when the 2FA code is confirmed in another call to the API. """
login(
"The credentials to use to log into the system."
credentials: LoginCredentials!
): LoginResult
}
"""
The `rID` scalar type represents a SportsX unique identifier, often used to
re-fetch an object or as key for a cache. The xID type appears in a JSON
response as a String; however, it is not intended to be human-readable.
When expected as an input type, any UUID formatted string will be accepted as an rID.
"""
scalar rID
"""
The result of a successful login or confirmation of a 2FA attempt. This object will contain
the information needed by the user when attempting to use authenticated endpoints. The
token returned needs to be put in an "Authorization" header with "Bearer {{TOKEN}}" as the
value where "{{TOKEN}}" should be replaced with the token returned by this API. This
object also contains the refresh token that is needed to refresh the token when it expires
without going through the login procedure.
"""
type LoginResult {
"The UUID of the user from the database."
userId: rID
"The integer status of the request."
status: Int
"The token to use for requests that requre authentication."
token: String
"The token used to refresh the token used for authenticated requests."
refreshToken: String
"The string version of the user status."
userStatus: String
"The unique user ID associated with the user with the external user service."
userUid: String
"The current profile for the user."
userProfile: UserProfile
"The ISO-8601 timestamp when the user last logged in."
lastLoginAt: String
"The ISO-8601 timestamp when the user last logged in for the current session."
currentLoginAt: String
"The IP address used by the currently logged in user."
deviceId: String
ipAddress: String
"The number of limits on the account."
limitsNumber: AccountLimitResultNumber
"Whether or not the client should prompt the user to supply a 2FA code."
promptTwoFactorAuth: Boolean
"The id of the user's current session."
sessionId: String
"Whether or not the client should prompt the user for TNC acceptance."
promptTncAcceptance: Boolean
"Which component of the TnC changed - terms, privacy_policy, house_rules etc"
tncChanged: TncChanges
"Id of the user's TNC acceptance record."
tncId: String
"Whitelisted IP addresses for this user."
whitelistIpAddresses: String
"The devices and device_tokens of this user."
deviceTokens: [DeviceTokens]
"Is multiple logins enabled for this user"
allowMultipleLogins: Boolean
}
You should take special note of the type rID
which is used all over the STX exchange api. This will always be a string encoded UUID such as 4ae3dc52-f4a1-45bc-91c7-ff47618b7d67
. STX uses UUIDs for every data type.
Examining the LoginResult
you can see the field promptTwoFactorAuth
as a possibility. If the account has two-factor authentication (2FA) enabled, the application will have to send a second graphql mutation call in order to complete the login process. The code for the 2FA confirmation will be sent in an email and the application will need to use the following mutation in order to complete the login process.
"User submitting the 2FA code in order to login."
confirm2Fa(
"The 2FA code that was sent to the user by email."
code: String!
"The id of the session to confirm 2FA for."
sessionId: String!
"The timestamp of the confirmation."
timestamp: Int @deprecated(reason: "Ignored by the backend")
"The email of the user that the confirmation is being made for."
email: EmailAddress!
): LoginResult
Once the login process has been completed the user will get the full LoginResult
data structure on the user. To see how this is used in practice consider the following query that can be run on the GraphiQL tool on the STX server.
mutation login($input: LoginCredentials!) {
login(credentials: $input) {
token
refreshToken
userId
userUid
promptTncAcceptance
promptTwoFactorAuth
sessionId
userProfile {
accountId
}
}
}
With Variables
{
"input": {
"email": "my_user@stxapp.io",
"password": "my_$ecure-Password",
"deviceInfo": {
"deviceId": "84661f84-ff37-4215-9c9b-0e47283003e6"
}
}
}
The caller sending the mutation will be able to specify the fields that they want in the LoginResult
and nested userProfile
. In the LoginResult
there are a lot of fields that give the user info about the account but probably the most important field is the token. This token will be needed to pass the authorrization header to calls that are protected such as placing orders. The token would be composed into a header such as:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjdiODcxMTIzNzU0MjdkNjU3…
Note that the actual token will be much longer than the example above.
Periodically the token will expire after some amount of time and thus the refreshToken exists. To retrieve a new token from the refresh token the user should call the newToken mutation shown below:
"Create a new authorization token from the user's refresh token."
newToken(refreshToken: String!): LoginResult
An example would be:
mutation refreshToken {
newToken("eyJhbGciOiJSUzI1NiIsImtpZCI6IjdiODcxMTIzNzU0MjdkNjU3")
}
Do not send the
token
from theLoginResult
to this mutation or you will get an error response. Make sure you send therefreshToken
from theLoginResult
. Not only will this generate a new token it will generate a new refreshToken for the next time a refresh is needed.
Once you are logged in you will be able to do protected actions like getting account history, making orders, fetching current orders and so on. In addition the token
will be used to connect to the various web sockets that the STX Exchange provides. When you are ready to log out, there is a mutation for that as well.
"The result of logging out of the platform."
type LogoutResult {
"The integer status of the request."
status: Int
"Message to display post-logout, if any."
message: String
"The id of the device that was logged out."
deviceId: String
"Th address of the user when the user logged out."
ipAddress: String
}
type RootMutationType {
"Logout of the platform on a specific device, invalidating all tokens."
logout(
"The information on the device to log out."
deviceInfo: DeviceInfo
): LogoutResult
}
Note that you have to supply the device info when logging out because the STX platform supports multiple devices. When you log in the device info that you pass should be used to logout. For example:
mutation Logout {
logout(deviceInfo:{deviceId:"84661f84-ff37-4215-9c9b-0e47283003e6"}) {
deviceId
ipAddress
message
status
}
}
User management is accomplished by means of updating the user profile. The user profile contains various data such as the address of the user, account limits and so on. The data definition is shown below.
"Input to the mutation to change a user's profile."
input UserProfileInput {
"The phone number for the user."
phoneNumber: String!
"The country_code of the phone number for the user."
countryCode: String
"The country code of the business phone number for the user."
businessCountryCode: String
"The business phone number for the user."
businessPhone: String
"The first line of the user's legal address."
address1: String!
"The second line of the user's legal address."
address2: String
"The city of the user's legal address."
city: String!
"The state of the user's legal address."
state: String!
"The zip code of the user's legal address."
zipCode: String!
"The middle name of the user."
middleName: String
"The preferred name of the user."
preferredName: String
"The country of the address."
country: String
"The country of citizenship of the user."
citizenshipCountry: String
"The employer of the user."
employerName: String
"The address of the employer of the user."
employerAddress: String
"The job title of the user."
jobTitle: String
"The industry in which the user works"
industry: String
"Opt-in for marketing emails, text messages"
optInMarketing: Boolean
}
"The result of a client reporting app usage when the user has time limit feature on."
type UsageResult {
"Maximum usage possible in seconds."
maxSecondsPerDay: Int
"Number of seconds already used."
secondsUsed: Int
}
"The device associated with a player."
type DeviceTokens {
deviceId: String
deviceToken: String
deviceType: String
effectiveStartDate: String
}
The user may alter some of this information within limits. Since STX is obligated by law to track users that participate in the platform, addresses changes and some other changes are subject to re-verification by our identity providers. To alter the user, call the updateUserProfile mutation shown below.
"Input to the mutation to change a user's profile."
input UserProfileInput {
"The phone number for the user."
phoneNumber: String!
"The country_code of the phone number for the user."
countryCode: String
"The country code of the business phone number for the user."
businessCountryCode: String
"The business phone number for the user."
businessPhone: String
"The first line of the user's legal address."
address1: String!
"The second line of the user's legal address."
address2: String
"The city of the user's legal address."
city: String!
"The state of the user's legal address."
state: String!
"The zip code of the user's legal address."
zipCode: String!
"The middle name of the user."
middleName: String
"The preferred name of the user."
preferredName: String
"The country of the address."
country: String
"The country of citizenship of the user."
citizenshipCountry: String
"The employer of the user."
employerName: String
"The address of the employer of the user."
employerAddress: String
"The job title of the user."
jobTitle: String
"The industry in which the user works"
industry: String
"Opt-in for marketing emails, text messages"
optInMarketing: Boolean
}
"The result of the mutation to update the user's address."
type UpdateUserProfileResult {
"A list of string errors."
errors: [String]
"The status of the request with the server."
status: Int!
"The user profile of the user as a result of the change."
userProfile: UserProfile!
}
type RootMutationType {
"Allows a user to update their profile with new information."
updateUserProfile(
"The data to update in the user profile."
input: UserProfileInput!
): UpdateUserProfileResult
}
In addition to the user's general information, the STX platform is required by law to implement restrictions on various financial numbers in the system. These are collectively referred to as limits. The system imposes some limits on the users, known as admin limits. The user can set account limits that are smaller than the admin limits if they desire. The user cannot exceed the admin limits on their account with account limits, the account limits exist for the purposes of users that want to limit their own participation. To increase the admin limits a customer will have to contact STX support and potentially provide further information to verify their identity. Market makers should contact support immediately upon registration as it is unlikely the default admin limits, which are set for the casual participant, are appropriate for the market maker. The limits on the account are shown in the structure below:
"""
Limits imposed on the account by the user. There is an analogous admin limits object
that sets the limits imposed by the system. The user can only adjust these limits up
to the values allowed by the admin limits.
"""
type AccountLimitsNumber {
"Account id that the AccountLimits apply to."
accountId: rID
"The total amount that may be deposited daily."
dailyDeposit: Int
"The total amount that may be deposited weekly."
weeklyDeposit: Int
"The total amount that may be deposited per month."
monthlyDeposit: Int
"The total amount that may be deposited for the life of the account.."
lifetimeDeposit: Int
"The maximum liability that may be incurred by all outstanding orders."
orderLiability: Int
"The maximum liability that may be incurred by any one order."
maxOrderLiabilityPerOrder: Int
"The loss limit value for the last 24 hours."
lossLimit24Hours: Int
"The loss limit value for the last week."
lossLimitWeekly: Int
"The loss limit value for the last month."
lossLimitMonthly: Int
"""
If a user has an hours per day limit set, this sets how long that user must wait after
the limit has been exceeded. Note that this is required by gaming regulations but does
not need to be used by customers.
"""
coolOffPeriod: Int
"An optional limit of the number of hours per day that a user can use the app."
hoursPerDay: Int
}
The limits of hours per day and cool off period may be confusing to market makers and professional traders. This is because these limits are required by gaming regulatory bodies so that a user can deal with problem gambling. However, if the user does not enable time limits per day then these limits do nothing. The other limits are required for gambling regulations, AML (Anti Money Laundering) and other regulatory bodies.
To query limits set on an account, the caller can use the following mutation.
type AccountLimitResultNumber {
"The account limits which can be edited but cannot exceed admin limits."
accountLimits: AccountLimitsNumber
"The admin limits which are imposed by the system"
adminLimits: AdminLimitsNumber
}
type RootQueryType {
"Account limits for a single account - values are returned as integer numbers"
accountLimitsNumber: AccountLimitResultNumber
"History of account limits changes by the user."
accountLimitsHistoryNumber: [AccountLimitHistoryResultNumber]
}
To change the account limits on an account, the user should invoke the mutation to change the limits.
"An enumeration of the available keys on a limit to change. Refer to AccountLimits type."
enum LimitKeys {
DAILY_DEPOSIT
WEEKLY_DEPOSIT
MONTHLY_DEPOSIT
LIFETIME_DEPOSIT
ORDER_LIABILITY
MAX_ORDER_LIABILITY_PER_ORDER
HOURS_PER_DAY
COOL_OFF_PERIOD
LOSS_LIMIT24_HOURS
LOSS_LIMIT_WEEKLY
LOSS_LIMIT_MONTHLY
}
"Result of asking for account limits containing both account and admin limits."
type AccountLimitResultNumber {
"The account limits which can be edited but cannot exceed admin limits."
accountLimits: AccountLimitsNumber
"The admin limits which are imposed by the system"
adminLimits: AdminLimitsNumber
}
type RootMutationType {
setLimitNumber(
"The key of the limit that the user wishes to change."
key: LimitKeys!
"The new value for the limit."
value: NonNegInt
"Optional password to set cool-off period"
password: String
): AccountLimitResultNumber
}
Other user management that can be invoked through the API includes changing password, resetting the user’s password, enabling and disabling 2FA, and requesting a new 2FA code. These mutations are shown here.
These mutations and data types enable a user to manage their 2FA settings.
"The result of enabling or disabling two factor authentication."
type TwoFactorAuthResult {
"The status of the request."
status: String
"The sessionId in which the 2fa code is stored."
sessionId: String
}
type RootMutationType {
"Enable 2FA on the currently logged in user's account."
enable2Fa(
"User's password to enable 2FA"
password: String
): TwoFactorAuthResult
"""
Disable 2FA on the currently logged in user's account. This mutation will do nothing if
the user does not have 2FA already enabled.
"""
disable2Fa(
"User's password to disable 2FA"
password: String
): TwoFactorAuthResult
"""
Request a new 2FA code providing the reason - i.e. updating user profile for example.
The reasons are enums defined above.
"""
send2Fa: TwoFactorAuthResult
"Request that another 2FA code be sent when apparently the user didnt get the code."
resend2Fa(
"The session id to resend the code for."
sessionId: String!
"The user id to resend the code for. The system will find the email for the user."
email: EmailAddress!
): TwoFactorAuthResult
"""
Enables tracking of all devices which have been verified with a 2FA code. This mutation
will do nothing if the user does not have 2FA enabled.
"""
enableDeviceTracking: TwoFactorAuthResult
"""
Disabled tracking of all devices which have been verified with a 2FA code. This mutation
will do nothing if the user does not have 2FA enabled or device tracking is not already
enabled.
"""
disableDeviceTracking: TwoFactorAuthResult
}
In addition to disabling and enabling 2FA there are a number of functionalities related to 2FA that are aavailable. Many of these functions center around actions that require a 2FA code to update the user's profile. If a mutation returns a response that a 2FA confirmation is neded, the correct mutation will need to be called to complete the action.
The user can call this mutation to reset their password should they forget it.
"The result of calling the forgot_password mutation."
type ForgotPasswordResult {
"The title to display in a modal dialog as a result of the call."
header: String
"The message to display in a modal dialog as a result of the call."
message: String
"The email address that the reset code was sent to."
email: EmailAddress
}
"The result of asking for a password to be reset."
type PasswordResetResult {
"Whether or not the reset was successful."
reset: Boolean
"Any message that was returned in the attempt for a reset."
message: String
}
"""
Parameters passed to the mutation to reset the user's password when they have forgotten
their password. This can only be called after the forgotPassword mutation and waiting
for the code to arrive in an email.
"""
input PasswordResetParams {
"The code that was sent to the user via email to reset the password."
code: String!
"The new password to use."
newPassword: String!
}
type RootMutationType {
"Invoked to initiate the forgotten password flow."
forgotPassword(email: EmailAddress!): ForgotPasswordResult
"Completes the forgotten password flow."
confirmPasswordReset(
"The parameters for resetting the password."
passwordResetParams: PasswordResetParams!
): PasswordResetResult
}
This mutation allows a user to change their password. Note that the user that has 2FA enabled will need to also call the mutation to confirm the 2FA code before the flow for changing the password can be completed.
"The result of enabling or disabling two factor authentication."
type TwoFactorAuthResult {
"The status of the request."
status: String
"The sessionId in which the 2fa code is stored."
sessionId: String
}
type RootMutationType {
"""
Change the current user's password to the password provided. Note that the means for making
sure a user typed the password properly are out of the scope of this method and left up
to the user interface calling this function.
"""
changePassword(
"The old password of the user."
oldPassword: String!
"The new password to use for the user's account."
newPassword: String!
): LoginResult
"Confirm the 2FA code for the password change."
confirmChangePassword2Fa(
"The 2fa code to confirm the changing of the old password to new password."
code: String!
): LoginResult
"Request that another 2FA code for password reset when the user didn't receive or use the previous 2fa code."
resendChangePassword2Fa: TwoFactorAuthResult
}
GraphQL provides the user the means to get information on their user in the system. The types and queries for this data are enumerated below.
"The profile of a user including any limits."
type UserProfile {
"The UUID of the user profile."
id: rID!
"The DOB of the user."
dateOfBirth: String
"The first name of the user."
firstName: String!
"The last name of the user."
lastName: String!
"The middle name of the user."
middleName: String
"The preferred name of the user."
preferredName: String
"The user name of the user."
username: String!
"The phone number of the user."
phoneNumber: String
"The country code of the phone number of the user."
countryCode: String
"The business country_code for the business phone number of the user."
businessCountryCode: String
"The business phone number of the user."
businessPhone: String
"The address1 part of the address of the user."
address1: String
"The address2 part of the address of the user."
address2: String
"The city of the user."
city: String
"The state of the user."
state: String
"The zip code of the user."
zipCode: String
"The country of the address."
country: String
"The country of the address."
citizenshipCountry: String
"The SSN of the user."
ssn: String
"The UUID of the account associated with this user."
accountId: rID
"The employer of the user."
employerName: String
"The address of the employer of the user."
employerAddress: String
"The job title of the user."
jobTitle: String
"The industry in which the user works"
industry: String
"Whether the 2FA is enabled for this user."
twoFactorAuth: Boolean
"Whether the 2FA is enabled per device for this user."
twoFactorAuthPerDevice: Boolean
"Opt-in for marketing emails, text messages"
optInMarketing: Boolean
"Whitelisted IP addresses for this user."
whitelistIpAddresses: String
}
type RootQueryType {
"A user's profile"
userProfile: UserProfile
}
The Market Info APIs give the user an ability to query for information about markets offered by the STX exchange. These APIs do not require authentication and are thus strongly hardened against flood attacks. This makes them very resilient and able to be called fequently. The developer should feel free to call these APIs on a regular cadence. That being said we will introduce the MarketInfo web socket later that will allow a user to receive realtime updates on the markets. In order to understand the API lets start with the MarketInfo data structure.
"""
The `PosInt` scalar type represents non-fractional signed whole numeric values greater
than or equal to 1
PosInt can represent values between `1` and `2^53 - 1` since it is
represented in JSON as double-precision floating point numbers specified
by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).
"""
scalar PosInt
"""
The `DateTime` scalar type represents a date and time in the UTC
timezone. The DateTime appears in a JSON response as an ISO8601 formatted
string, including UTC timezone ("Z"). The parsed date and time string will
be converted to UTC if there is an offset.
"""
scalar DateTime
"A data structure that describes the bid or offer for the market."
type BidOrOffer {
"The price of the bid or offer."
price: Int
"The quantity of contracts of the bid or offer."
quantity: Int
}
"A rule that instructs the UI on the best way to increment and decrement the price."
type PriceRules {
"The smallest price for the rule."
from: Int
"The greatest price for the rule."
to: Int
"The amount ot increment when the user presses an increment button."
inc: Int
}
"The information that describes a single participant."
type Participant {
"The abbreviation of the participant."
abbreviation: String
"The name of the participant."
name: String
"The role of the participant in the event, e.g: 'home' or 'away'."
role: String
}
"A filter that can be passed to market info queries."
type MarketFilter {
"The category of market to include or null if not filtered."
category: String
"The subcategory of market to include or null if not filtered."
subcategory: String
"The section of market to include or null if not filtered."
section: String
"The grouping of the market within its section. This may be `nil` or and empty string."
grouping: String
"Whether this filter has been set manually or automatically."
manual: Boolean
}
"A data structure that defines an anonymized recent trade."
type RecentTrade {
"The price of the trade."
price: Int
"The number of contracts the trade executed for."
quantity: Int
"Whether the `buyer` or the `seller` took the liquidity."
liquidityTaker: String
"The ISO8601 formatted string of when the market info was created same as the `timestampInt`."
timestamp: String
"The UNIX microseconds timestamp of when this market info was created."
timestampInt: Int
}
"The information that describes a single market."
type MarketInfo {
"Is the market archived or not."
archived: Boolean
"The available bids on the market."
bids(
"An optional integer, greater than zero, to limit the number of results."
limit: PosInt
"An optional sort. By default bids are sorted highest to lowest"
sort: SortOrder
): [BidOrOffer]
"The UTC time when the market was resulted or voided."
settledAt: String
"The description of the market."
description: String
"The string that can be used for the detailed brief on the market's event."
detailedEventBrief: String
"The string that can be used for the brief on the market's event."
eventBrief: String
"The id of the event that the market is attached to."
eventId: rID
"The UTC start date and time of the event."
eventStart: String
"The status of the event that the market is attached to."
eventStatus: String
"The type of event associated with the market."
eventType: String
"The list of filters in which the market appears."
filters: [MarketFilter]
"The category in which the market appears: `Upcoming`, `Live` or `nil`."
homeCategory: String
"The time that the last probability was received by the server."
lastProbabilityAt: DateTime
"The price of the last executed trade."
lastTradedPrice: Int
"Is the probability manual or from odds provider."
manualProbability: Boolean
"The unique id of the market"
marketId: rID
"The max price for orders and settlements in this market."
maxPrice: Int
"The available offers on the market."
offers(
"An optional integer, greater than zero, to limit the number of results."
limit: PosInt
"An optional sort. By default offers are sorted highest to lowest"
sort: SortOrder
): [BidOrOffer]
"A JSON array describing the rules how the order's price should increment\/decrement on the UI."
orderPriceRules: [PriceRules]
"The text to use in describing the position."
position: String
"The text to use in describing the sport, e.g. Basketball."
sport: String
"The text to use in describing the competition, e.g. NBA."
competition: String
"The map of participants in the market."
participants: [Participant]
"The keywords that are set for the market."
keywords: [String]
"What is the probability for the market"
probability: Float
"The market price that the market is trading at."
price: Float
"The change in price over the last 24 hours."
priceChange24h: Int
"The question that the market is asking."
question: String
"A list of the last 15 recent trades on the market."
recentTrades: [RecentTrade]
"The result of the market."
result: String
"The status of the market."
status: String
"A unique symbol for this market."
symbol: String
"The human readable short title for the market."
shortTitle: String
"The human readable group title for the market."
groupTitle: String
"The human readable title for the market."
title: String
"The ISO8601 formatted string of when the market info was created. See `timestampInt`."
timestamp: String
"The UNIX microseconds timestamp of when this market info was created."
timestampInt: Int
"The list of filters to use for organizing trades, settlements and related items."
tradingFilters: [MarketFilter]
"The specifier for the rules to properly determine the market."
specifier: String
"The rules for this market."
rules: String
"Trade volume this market has had in the last 24 hours"
volume24h: Int
}
There is a lot of information in this data type as it forms the core backbone of our server communication with our front end applications. Some items like order_price_rules
will probably be of little use to a trader integrating with our exchange. They are there because our iOS application is built to be driven by the backend, allowing us a massive amount of flexibility in introducing new events, market types and so on. The beauty of graphql is that the user can decide what they want out of the MarketInfo
and get back only that data. The structure for the MarketInfo
query is shown below.
"Query parameters for the `marketInfos` query"
input MarketInfosInput {
"A list of market ids to return settlements for. If specified, 'limit' is ignored."
marketIds: [rID!]
"The type of filter that should be applied"
filterBy: MarketFilterEnum
"The specification of markets to fetch by filter name"
marketFilter: MarketFilterType
"Number of records to constrain the return. 'nil' and '0' will not limit results"
limit: NonNegInt
}
"Filter options for limiting market_infos."
enum MarketFilterEnum {
"""
Return active markets (default).
A market is 'active' if it's 'pre_open', 'open', 'suspended' or
('close' or 'cancelled' but not yet archived)
"""
ACTIVE
"""
Return markets whose homeCategory is "Live" or "Upcoming"
If there are any 'Live' markets (regardless of the number), no 'upcoming' are returned.
"Upcoming" markets are only those that have status "open"
If there are neither "Live" nor "Upcoming" markets, an empty list will be returned.
Limited to a maximum of 10 records. The 'limit' parameter can further constrain the results.
"""
LIVE_AND_UPCOMING
}
"Parameters for selecting markets based on filter tree"
input MarketFilterType {
"Which type of filter to query on"
group: MarketFilterGroupEnum!
"A path (list of node names) to a leaf in the market filter tree to set of markets"
path: [String!]!
}
type RootQueryType {
"Returns a list of the markets and filtered details"
marketInfos(
"Optional list of IDs of the marketInfos to return"
input: MarketInfosInput
): [MarketInfo]
}
Note that we add capabilities to this query on a regular basis based on user feedback. Although this might be a bit difficult to take in, lets consider how an odds integrator might use this to show STX prices on their site.
query MarketInfos {
marketInfos(input: { filterBy: LIVE_AND_UPCOMING }) {
marketId
shortTitle
title
sport
competition
detailedEventBrief
status
result
participants {
abbreviation
name
role
}
bids(limit: 1) {
price
quantity
}
offers(limit: 1, sort: ASC) {
price
quantity
}
}
}
In this query the integrator wants to know the best bid and offer in the market in order to calculate the current odds and liquidity on the market. They could remove any of the fields if they were not of interest. An example result of this query is.
{
"data": {
"marketInfos": [
{
"bids": [
{
"price": 5701,
"quantity": 35
}
],
"competition": "NFL",
"detailedEventBrief": "Today at 01:00 PM EDT",
"marketId": "956b1784-2bc5-4e8b-a5ea-6d478f9a8085",
"offers": [
{
"price": 5950,
"quantity": 49
}
],
"participants": [
{
"abbreviation": "ATL",
"name": "Atlanta Falcons",
"role": "away"
},
{
"abbreviation": "TB",
"name": "Tampa Bay Buccaneers",
"role": "home"
}
],
"result": "pending",
"shortTitle": "ATL @ TB",
"sport": "Football",
"status": "open",
"title": "NFL - Week 7 ATL @ TB"
},
{
"bids": [
{
"price": 2030,
"quantity": 98
}
],
"competition": "NFL",
"detailedEventBrief": "Today at 01:00 PM EDT",
"marketId": "c33a6e27-2177-402e-ac5d-ed98b235a85e",
"max_price": 10000,
"offers": [
{
"price": 2314,
"quantity": 26
}
],
"participants": [
{
"abbreviation": "BUF",
"name": "Buffalo Bills",
"role": "away"
},
{
"abbreviation": "NE",
"name": "New England Patriots",
"role": "home"
}
],
"result": "pending",
"shortTitle": "BUF @ NE",
"sport": "Football",
"status": "open",
"title": "NFL - Week 7 BUF @ NE"
}
]
}
}
With a bit of math this code will allow the integrator to determine the odds on the event. If you are interested in this kind of integration we have a full guide available on the wiki.
For the company that wants to integrate their client into the STX exchange they can use other fields like filters to categorize the markets and then search based on the filters. For example consider this query.
query MarketInfos {
marketInfos(
input: { marketFilter: { path: ["NFL", "Games"], group: BASIC } }
) {
marketId
shortTitle
}
}
This query will return the marketID
and shortTitle
for all markets that have the category NFL
and subcategory Games
using the BASIC
categorization. There are two kinds of categorization we use in the app. The BASIC
categorization changes quite frequently depending on the state of the game and other factors. The TRADE
categorization doesn't change much if at all and is more centered on the participants in the event. We would suggest that the integrator look through and understand the MarketInfo
data to gain a full undertanding of the dynamics.
There are literally thousands of ways this API can be used and it is up to the integrator to pick and choose what they want. However, if you need a certain feature that we dont have yet then please dont hessitate to ask. We are here to help.
On one final note, this endpoint is very strongly hardened against DDOS spam. It is unlikely that you would be able to materially affect the server before our network controls blocked your IP. With that said, unless you are intentionally trying to DDOS the endpoint it is unlikely you will generate enough requests to cause the network locks to come down so please feel free to call it frequently. One odds integrator warned us that he might call it every 30 seconds and we assurred him that is no problem. The STX server was designed to be scalable enough to take the load of something like the BITMEX exchange.
There are multiple ways to trade on the STX exchange but the most basic of these is to buy and sell a market in the same way that an option in traditional financial markets are traded. The term for a "share" in this kind of market is a "contract." Each market in the STX exchange is a binary outcome. Either the market succeeds or does not. If we query for the description in MarketInfo
we might see something like:
{
"data": {
"marketInfos": [
{
"description": "Contracts for this market settle into $100 if the Boston Red Sox beats the New York Yankees by at least 1.5 runs and $0 if they do not.",
"marketId": "becf9bf9-e2ff-4b50-879b-46054ad5c69a",
"shortTitle": "NYY @ BOS -1.5"
}
]
}
}
You can see that this market is very specific as to the outcome and the result will be to settle all outstanding contracts for $0 or for $100 depending upon the score in the game.
In this case if we want to back the Yankees we would sell this market. If we wanted to back Boston we would buy in this market. Whether you are selling or buying there are two possible types of orders that can be placed.
A limit order specifies a ceiling for a buy order or a floor for a sell order. When a user wants to buy for no more than $40 per contract they place a limit buy order using a $40 limit. Once they place the order the STX exchange will go through the order book and find them the best price for their order. If someone is offering to sell for $35 per contract then they will get that price instead until there is no more quantity at $35. The order book will continue to match until there are no more contracts available at $40 or the order is filled. If the order is not filled by that time then the order will be left in the order book to be filled when someone else makes a matching sell order. This all happens in microseconds. Any unfilled quantity of an order will be able to be cancelled but the user will be in a race with other users trying to fill against the order so it cant be guaranteed that the cancel works.
A market order is like a limit order except the ceiling for the buy order is the max price of the market (usually $100) and the floor for sell orders is $0.01 and market orders do not stay in the book if not filled. Any quantity not filled on a market order will be cancelled. For example, if a user placed a market order to buy 50 contracts and only 40 were available, the 40 would be filled and trades executed but the remaining 10 would be cancelled.
Its important to understand that if you want to be a market maker and name a price you must use a limit order. Market orders are used only for taking liquidity placed by others.
The account and wallet model of the STX Exchange is quite a bit more complicated than the traditional sports book. In a traditional book the user makes a bet and then that bet is registered in the system and the stake deducted from the wallet. Upon completion of the event, the user will be paid out for their stake multiplied by the odds. On the other hand the STX Exchange wallet contains a variety of liabilities and other numbers that the user needs to understand.
Upon order placement an Order Liability will be incurred. The order liability is the additional amount of risk that the user incurs by placing the order. Thus, assuming no prior position on that market, the order liability will reduce the user's available balance by the amount of that risk incurred by the order. For a buy order, the risk is merely the price of the limit price of the order. For a sell order the risk is the max price of the market minus the limit price of the order. Since most markets have a $100 max price the risk of a sell order with a limit price of $36 would be $100 - $36 = $64 per contract.
When a trade is executed on an order, the order liability is converted to position liability. Note that filling an order can, and frequently will, result in an increase in available balance. For example, if a limit order was placed to buy 10 contracts for $45 and the order is partially filled, buying 5 contracts for $43, then the total position liability would be 5 * $43 = $215 and the remaining order liability would be 5 * $45 = $225. In total the user’s liabilities would be $440, rather than the original $450 of risk before the order was partially filled. This trade would result in the available balance increasing by $10.
Placing orders does not always incur additional liability. Continuing on the example above, if the user filled all 10 contracts for $45 they would take on a risk of $450. The user could then place a sell order to sell 5 contracts for $60 to try to capitalize on a profitable movement in the market. In a vacuum this sell order would incur a liability of 5 * (100-60) = $200. However, since the user already has a long position from buying 10 contracts earlier, this sell order executing would incur a settlement rather than opening a new position. In this situation, the placing of the sell order does not change the available balance because the total risk to the user remains the same. If the sell order executes, their available balance will actually increase because it reduces the risk of 5 of the initially bought contracts going to $0 and in fact generates a profitable settlement of 5 * (60-45) = $75 which gets added to available balance. At this point the user has no orders open so their order liability is $0, and their position is now long 5 contracts at a market price of $60, so their positional liability is $300. Finally, their max possible loss is only $225 because they have already secured a $75 gross proffit.
When a trade is closed by buying against a market in which the user is short or selling against a market in which the user is long or by the market expiring, the money in the system is finally moved. The remaining positional liability is cleared by the settlement of the trade. For example, if a market closed with a positive result and the user had a $215 liability outstanding on 5 contracts, the trades would expire and their liability would be released and they would get another $285 in gross settlements. If the market has not closed a user could also sell the contracts in that market to free up available balance by settling part or all of the position and thus diminishing the existing liability.
The STX Exchange tracks these liabilities for the user and informs them, via sockets, when these numbers change. All transactions in the system are 100% capitalized; there is no margin trading as of yet in the system. The exchange has many different fee models that will alter these numbers in real situations depending upon the customer. The example above assumes 0 fees.
Now that we understand how orders in the STX exchange work, we can learn how to make and cancel orders.
The user can use GraphQL mutations to place and cancel orders in the system. However, it should be noted that these mutations don't execute an absolute result for the users. In fact they are just requests for the order placement or cancellation. For example, a user could request to place an order to sell contracts and the contracts may not be offered for sale in the market by the time the order is processed. Similarly, a user could request that an order be canceled but that will be dependent on the conditions of the system. By the time the cancellation reaches the system the order may have already been executed and traded. The system is never held static and moves very fast.
To truly limit losses the user should use limit orders to execute at precise times. For example, having bought 10 contracts at $40 I could immediately place an order to sell for at least $60. Of course even these orders are dependent on the system status. There could be 10 users ahead of you with sell orders. With this in mind, let's see how we place and cancel orders in the STX Exchange.
To place an order we call the confirmOrder GraphQL mutation which looks like the following:
"Defines a scalar price."
scalar Price
"Geo Location code"
scalar GeoLocationCode
"Possible actions that can be used in orders."
enum OrderType {
"""
A market order which doesnt constrain the price. Market orders buy or sell shares at the
best possible price until they are filled or no more quantity is available. If a market
order cannot be completely filled the unfilled quantity is cancelled.
"""
MARKET
"""
A limit order which defines a floor (for sell) or ceiling (for buy) price. The system
will try to buy or sell shares at the best price possible until it hits the floor or
ceiling limit price. Remaining unfilled amounts stay in the order book and could be
filled at any time later by arriving orders.
"""
LIMIT
}
"Possible actions that can be used in orders."
enum OrderAction {
"An order to buy contracts."
BUY
"An order to sell contracts."
SELL
}
"Encapsulates information needed to place an order a market."
input UserOrder {
"The ID of the market to place the order on."
marketId: rID!
"The type of order to place."
orderType: OrderType!
"The action of the order."
action: OrderAction!
"The limit price for an order, which will be ignored for order_type `market`."
price: Price
"The quantity of the order in number of contracts."
quantity: Int!
}
"The result of a user attempting to place an order."
type OrderResult {
"The order that was placed if the request was successful."
order: Order
"The list of errors that occurred, if any."
errors: [String]
}
type RootMutationType {
"Creates a new order."
confirmOrder(
"The data structure holding the details for the order to be placed."
userOrder: UserOrder!
geoLocation: GeoLocationCode
): OrderResult
}
There are a lot of data types in this but the most important thing to understand is that this a request for an order, not a guarantee. To see how this is used, lets examine an example:
mutation PlaceOrder {
confirmOrder(
deviceInfo: "MyTradingApp#1234"
geoLocation: 9915363262344334
userOrder: {
action: "BUY"
clientOrderId: "27187"
marketId: "4ae3dc52-f4a1-45bc-91c7-ff47618b7d67"
orderType: LIMIT
price: 4500
quantity: 10
}
)
}
In this case the user is placing an order to buy 10 contracts at $45 per contract. Note that the prices are in cents; or whatever is the smallest fraction of currency in the locale. Since this is a limit order, if it is not matched it will stay in the book waiting for a user to match the order until cancelled. In addition, there is the ability for a client to pass their own order ID from their own system. This clientOrderId
will be available as a data element on any queries concerning the order but it must be a string. Finally, there is the geoLocation
code. This allows us to make sure the user placing the orders is in the correct jurisdiction.
STX uses GeoComply to validate that the users of our platform are within jurisdiction and comply with laws. If your implementation is using the API directly you may have to implement GeoComply in your own system. We are experts at helping our customers make sure they are meeting these requirements. Our C# SDK, for example, integrates GeoComply for the customer. There are also utilities to integrate it into a browser and other options. We MUST validate user and hardware location by law in most jurisdictions. If you are having trouble with this, please contact STX support and we can help.
In contrast to placing orders, cancelling orders is much simpler. The GraphQL mutations for this activity look like the following:
"The result of requesting to cancel a single order."
type CancelOrderResult {
"The status of the order that was requested to be cancelled."
status: String
}
type RootMutationType {
"Cancels an order."
cancelOrder(
"The id of the order that the user wishes to cancel."
orderId: rID!
geoLocation: GeoLocationCode
): CancelOrderResult
}
This mutation will cancel any single order and can be used as in the example below:
mutation PlaceOrder {
cancelOrder(
orderId: "758fae8f-c210-4f01-bbbc-a3eb5cc74cd1"
}
}
Keep in mind that the order may already be filled before your cancel request reaches the server and the market. In an exchange the orders are moving extremely fast, much faster than in a traditional betting environment, and thus someone may have already placed an order to fill the order you are trying to cancel. Once you get the result of the cancel you can always query the API to see how many contracts were filled and so on. In addition the sockets will, of course, send any trades that occurred on the order so they can be consumed realitime.
In many circumstances a market maker may wish to cancel more than one order at a time. STX has the ability to do that as well:
"The result of requesting to cancel a batch of orders. One of these is listed per order."
type BatchCancelOrdersResult {
"The order id that was requested to be cancelled.."
orderId: rID
"The status of the order id after the attempted cancellation."
status: String
}
type RootMutationType {
"Batch cancel of orders."
cancelOrders(
"The list of the ids of the orders that the user wishes to cancel."
orderIds: [rID]
geoLocation: GeoLocationCode
): [BatchCancelOrdersResult]
"Cancel of all open orders of an account."
cancelAllOrders(geoLocation: GeoLocationCode): [BatchCancelOrdersResult]
}
The reader should take notice that the mutation will return an array of results, one for each order, which will contain the actual result of the requested cancelation.
In addition to placing and cancelling orders, the user can query the history of orders, trades and settlements in the system. It should be understood, however, that the system moves very fast and information from a query could change milliseconds later. To have a true real-time picture of what is going on in the platform, the user should connect to our sockets and receive push messages from the socket.
To query for order history the user can use the following API.
"Data structure for pagination."
input Pagination {
"The page of result where each page has `limit` values. Integer >= 0"
page: NonNegInt!
"The limit of values per page. Integer value greater than 0."
limit: PosInt!
}
"A return result from mutations that return order lists."
type OrdersWithCount {
"The total count of orders in the list."
totalCount: Int
"The list or orders returned."
orders: [Order]
}
"A sort order that can be passed to a query requesting a list of orders."
input OrdersSortBy {
"The name of the field to sort by."
name: OrdersSortByField
"The direction, `asc` or `desc` for the sort."
direction: SortOrder
}
type RootQueryType {
"List of all orders for a user"
myOrderHistory(
"The id of the market to restrict the orders returned to."
marketId: rID
"The pagination to use for the results."
pagination: Pagination
"The sorting to use for the results."
sortBy: OrdersSortBy
): OrdersWithCount
}
When it comes to trades there are similar queries. Keep in mind, however, that the number of trades will far exceed the orders. An order could have anywhere from 0 trades to thousands of trade based upon the market and order parameters. To get the trade history use the following API.
"Result type that returns a list of trades along with the count in the list."
type TradesWithCount {
"The total count of trades in the list."
totalCount: Int!
"The list of the trades."
trades: [Trade]!
}
"Filter for sending market ids in a query for trade history."
input MarketIdsFilter {
"A list of market ids to return trades for."
marketIds: [rID]
}
"Defines possible values that can be used for sort order."
enum SortOrder {
"Sort the values in ascending order."
ASC
"Sort the values in descending order."
DESC
}
"Trade's sorting attribute"
input TradesSortBy {
"The name of the attribute to sort on, default is `time`."
name: TradesSortByField
"The direction to sort, either `asc` or `desc`"
direction: SortOrder
}
type RootQueryType {
"List of the user's trades"
myTradesHistory(
"How to sort the results of the query."
sortBy: TradesSortBy
"The id of the market to restrict the trades to."
marketId: rID
"A filter for market ids to include."
filter: MarketIdsFilter
"The pagination to use for the results."
pagination: Pagination
): TradesWithCount
"List of trades associated with provided order id"
myTradesForOrder(
"The id of the order to find the trades for."
orderId: rID!
): [Trade]
}
There are a number of filters to be used to narrow down the trade search and we advise the integrator use as many as they can. The pagination has a maximum value that can be set and will always be implied if not passed. The server uses the pagination to protect itself from excessive queries.
Settlements are created when either a trade closes an existing position or a market expires. Settlements translate directly to transactions and money movement in the system. To query settlements the following API can be used.
"Encapsulates an account's history of settlements."
type NetSettlementHistory {
"The total count of settlements returned."
totalCount: Int
"The settlement objects."
settlements: [Settlement]
}
type RootQueryType {
}
The STX API contains a number of queries and mutations that allow a user to manage their account and wallet. These features are used by our client applications and are available to the user but for the most part it would be best to use the application to deposit and withdraw using retail means. Market makers will usually use something like a wire transfer. If you need to do a wire transfer ourr bank information is available in the app and a STX customer service agent will help you complete the transfer. For retail transactions such as Interrac e-Verify, Credit Cards, and so on the APIs below implement these flows. However, orchestrating them in the right order is a bit complex and that is why we suggest using the app. That being said, lets discuss the APIs.
For the small stakes users, depositing and withdrawal is via a Paysafe transaction. Most of the main deposit flow is implemented on the client side for protection of private information. After the user initiates the transaction on Paysafe or Inteerac, the client the calls the STX exchange with the token and the amount of the deposit which the STX Exchange confirms with Paysafe before crediting a payment to the user or withdrawing funds. The GraphQL mutations that accomplish this are below.
"Defines possible Paysafe's API to use for deposits."
enum PaysafeDepositApi {
"'paymenthub' API"
PAYMENTHUB
"API used by iOS SDK for card payments"
CARD
}
"Defines possible Paysafe's deposit types"
enum PaysafeDepositType {
"Credit card"
CARD
"'paymenthub' API"
INTERAC
"API used by iOS SDK for card payments"
VIPPREFERRED
}
"Defines possible Paysafe's withdrawal types"
enum PaysafeWithdrawalType {
"'paymenthub' API"
INTERAC
"API used by iOS SDK for card payments"
VIPPREFERRED
}
"A return result for the result of a payment transaction."
type PaysafeTransactionResult {
"The id of the payment record, if created."
paymentId: ID
"The status of the transaction."
status: String
"The available balance after the transaction."
availableBalance: Int
"The amount of the transaction."
amount: Int
"The message to use for the transaction."
message: String
"Paysafe error code in case of failure. `null` if transaction failed before talking to provider."
errorCode: Int
"Action required by the user to finalize the payment. Can be REDIRECT or NONE"
action: PaysafeAction
"In case of REDIRECT action, the link to redirect the user to."
link: String
}
type RootMutationType {
"Called to deposit money in a user's account via paysafe."
paysafeDeposit(
"The amount to deposit which must match the amount from the token."
amount: PosInt!
"The optional handle obtained from Paysafe for the deposit. If not provided it will be generated by the server"
handle: String
"The optional id of the handle obtained from paysafe for the deposit."
handleId: String
"The method of deposit such as credit card."
method: PaysafeDepositApi @deprecated(reason: "Use `api`")
"The Paysafe API to use when making the deposit."
api: PaysafeDepositApi
"Paysafe's payment type. In example: interac, vip-preferred"
type: PaysafeDepositType!
"An optional description to add to the deposit."
description: String
): PaysafeTransactionResult
"Called to withdraw funds from the platform, currently only ACH paysafe supported."
paysafeWithdrawal(
"The amount to withdraw from the user's account."
amount: PosInt!
"The handle obtained from Paysafe for the withdrawal. Can be null, in this case the withdrawal will wait for approval."
handle: String
"The id of the handle obtained from paysafe for the withdrawal."
handleId: String
"The Payment method, currently `card` or `paymenthub` is supported."
method: PaysafeWithdrawalApi!
"Paysafe's payment type."
type: PaysafeWithdrawalType!
"Optional description of the payment."
description: String
): PaysafeTransactionResult
}
It's important to get the timing and order of calls correct in this situation and this is why we, once again, encourage users to utilize our applications to accomplish this.
In order to inform the customer of real-time changes in data, the STX Exchange employs web sockets based on the Phoenix Web Socket package. As was discussed in the introduction it is highly advisable that the integrator use one of the many libraries available to parse and manage the socket communication. This will accelerate the integration greatly. Furthermore, we advise that the integrator download a browser based web sockrt plugin to explore the usage of the sockets. Note that these browser based plugins will require doing a lot of tasks manually. The rest of th\is document will describe those manual tasks.
Most of the sockets in the STX Exchange require authentication to connect to them. They use the same tokens that the GraphQL API mutations and query require. The process to get the token is the same. The usage of the token will depend on the web sockets. Most implementations will require the token to be passed to the implementation and handle the details from there.
The STX Exchange sockets will batch information to avoid flooding the client. For example, Markets will send price updates only about once per second. This also goes for user trades, order changes and settlements.
After having obtained a token using the login process, that token can be used to connect to the socket. The integrator should keep in mind that if the token expires they will have to reconnect to the socket after having obtained a new token. Using the refresh token described in previous sections is the easiest way to get a new token. The developer can accomplish the connection by using a url in the following form and replacing the <<TOKEN HERE>>
with the correct token:
wss://api-staging.on.sportsxapp.com//socket/websocket?token=<<TOKEN HERE>>&vsn=2.0.0
For example this would be a valid URL for the author at the time of writing:
wss://api-staging.on.sportsxapp.com//socket/websocket?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJzdHgtYXBwIawiZW1haWwiOiJhZG1pbkBzdHhhcHAuaW8iLCJleHAiOjE2OTkyMTIxNDcsImlhd333TY5OTIwODU0NywiaXNzIjoiU1RYIiwianRpIjoiMnVhY3FhdjUyZ2VzNnA22hzMHZrcG4zIiwibmJmIjoxNjk5MjA4NTQ3fQ.4taQvT4cKcJxf8rwKdW0Lzaq_KtwtH6OFghXE0tGSOM&vsn=2.0.0
Once connected the user will have to issue a join command that is specific to each of the channels and will be discussed in the sections below. All sockets have a keep-alive timeout, usually 20 seconds, that require a user to send a ping in order to keep the channel open. Some channels have also a configurable timeout.
This channel gives information about active markets. The main point of this channel is to provide
more optimized way of live updates that happen to a market. When something in the market changes
the market_update message will be sent to the channel. Content of the message is a JSON object containing mandatory market_id and timestamp keys. This helps to identified which market changed
and when. Other than this, the object can contain any of the fields that are returned by the /api/market_info
endpoint provided that they changed.
MarketInfo object
in order to integrate with this channel the user must join by opening the socket and sending the following message.
["3","3","market_info","phx_join", ""]
Most of the details of the this message are facets of phoenix sockets but this would work in a browser tool. At this point the socket will start feeding changed data from the market. This specific channel gets a lot of traffic so it only sends the diffs of the market. If the user obtains the MarketInfo
for a particular market, they can keep up to date with the changes by consuming this channel and replacing keys in the map with the updated ones. Our C# and iOS SDKs already handle this work for the user. If you are using another integration method we suggest that you have a local data repository that keeps things up to date and then periodically query the server for the markets to make sure a message wasnt lost in the fluctuations of the internet.
When a market is updated, even though only changed fields will be included in the pushed message, every push will have the mandatory market_id and timestamp fields indicating what market change. Market status changes are pushed immediately when they happen. All the other changes like bids
offers
, recent_trades
, event_brief
and so on are send on a cadenceto avoid flooding the clients. An example of a status change message is shown below.
[
null,
null,
"market_info",
"market_updated",
{
"017511eb-930b-492a-8933-2284067e3039": {
"market_id": "017511eb-930b-492a-8933-2284067e3039",
"status": "pre_open",
"timestamp": "2021-03-24T13:11:42.113364Z"
}
}
]
The first two fields are internal to Phoenix sockets, the third is the channel name, the fourth is the type of message sent and the rest is a payload. In this case the payload is a JSON map keyed by market id. An example of the message sent when an event, such as a game, starts is here:
[
null,
null,
"market_info",
"market_updated",
{
"256910ed-213c-44bb-8b9b-66d48689e42b": {
"event_brief": "PHX 0 - 0 ORL : Q1 12:00",
"event_status": "in_progress",
"market_id": "256910ed-213c-44bb-8b9b-66d48689e42b",
"timestamp": "2021-03-24T13:24:51.138859Z"
}
}
]
When new markets are created a message will be sent to the connected users with the entire MarketInfo
of the new market. The message will look something like this:
[
null,
null,
"market_info",
"market_created",
{
"e3156733-502a-495e-a0cf-5d1dda04b3c6": {
"conference": "Eastern",
"description": "Contracts for this market settle into $100 if the Chicago Bulld defeat the Cleveland Cavaliers and $0 if not.",
"game_time": "2021-03-25T00:00:00Z",
"result": "pending",
"filters": [
{
"category": "NBA",
"section": "Week 14",
"subcategory": "Game"
}
],
"event_brief": "Starts March 25, 00:00",
"division": "Central",
"question": "Will the Chicago Bulls defeat the Cleveland Cavaliers?",
"timestamp": "2021-03-24T12:58:30.869725Z",
"manual_probability": false,
"title": "NBA - Week 14 CLE @ CHI",
"event_status": "scheduled",
"status": "open",
"event_type": "nba_game",
"position": "Chicago Bulls",
"market_id": "e3156733-502a-495e-a0cf-5d1dda04b3c6",
"market_type": "Game",
"bids": [],
"event_start": "2021-03-25T00:00:00Z",
"id": "e3156733-502a-495e-a0cf-5d1dda04b3c6",
"offers": [],
"rules": "home_winner",
"specifier": null,
...
}
}
]
Note that this is an abbreviated version of the market_created
message as there are quite a few fields in the MarketInfo
object and the reader should refer to the previous GraphQL sections for a full description. This market is a home_winner
market which translates to two traditional moneyline markets in a traditional book.
When the user recieves this message, they should add the market to their internal storage and start tracking changes if interested in the market. There are some markets that will not interest some customers and the messages for these markets can be dropped and ignored. There is currently now mechansims by which a user can ignore messages from certain markets serverside though that is a planned feature.
The portfolio channel delivers updates to users' available balance and other portfolio numbers. The iOS app uses this channel to keep the wallet amounts up to date. Note, however, that the server doesn't calculate the current market value of a user's portfolio as this would be cost prohibitive given how fast prices can change and the number of customers. It is up to the integrator to read MarketInfo
and portfolio updates and keep a running total of total portfolio value. To join the channel connect to the web socket and issue the following command after replacing {{UUID}}
with the UUID of your user, which can be found in the login result.
["3","3","portfolio:{{UUID}}","phx_join", ""]
The schema for the updates on the portfolio channel is as follows:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/porfolio_channel_schema",
"title": "Portfolio",
"type": "object",
"description": "Information on a user's portfolio",
"properties": {
"account_id": {
"description": "The ID of the account",
"type": "string",
"format": "uuid"
},
"available_balance": {
"description": "The amount of cash the user has available.",
"type": "number",
"format": "integer"
},
"buy_order_liability": {
"description": "The liability the user has because of current buy orders.",
"type": "number",
"format": "integer"
},
"sell_order_liability": {
"description": "The liability the user has because of current sell orders.",
"type": "number",
"format": "integer"
},
"position_premium_liability": {
"description": "Liability from position premiums.",
"type": "number",
"format": "integer"
},
"user_id": {
"description": "ID of the user that owns the account for the portfolio.",
"type": "number",
"format": "integer"
}
}
}
This channel will only send updates when changes to the user's available balance happen. This will only occur in one of 4 use cases:
It is important to remember that market price changes will not result in messages. Tracking the market value of a portfolio, as explained earlier, is a client task. It also is important to remember that available balance can go up as a result of creating a trade.
All authenticated socket channels require the user to send a heartbeat ping at regular intervals to keep the server from automatically closing. A message like the following can be sent to the channel:
["1", "1", "portfolio:{{UUID}}", "ping", ""]
Another channel that delivers information on a user's portfolio is the Active Positions channel. This channel is designed to deliver updates on markets where a user currently has some position, either long or short. To join the channel the following command can be used with the {{UUID}}
replaces with the user's UUID:
["3","3","active_positions:{{UID}}","phx_join", ""]
The JSON schema for the messages on the channel is below:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/active_trade_schema",
"title": "ActivePosition",
"type": "object",
"description": "Information on a position in the active positions channel",
"properties": {
"market_id": {
"description": "The unique id of the market the trade that was settled was on.",
"type": "string",
"format": "uuid"
},
"position": {
"description": "The position of the user which will be positive if long and negative if short.",
"type": "number",
"format": "integer"
},
"premium": {
"description": "The premium paid to acquire the position.",
"type": "number",
"format": "integer",
"minimum": 1
},
"buy_order_liability": {
"description": "The liability of all outstanding buy orders.",
"type": "number",
"format": "integer",
"minimum": 0
},
"sell_order_liability": {
"description": "The liability of all outstanding sell orders.",
"type": "number",
"format": "integer",
"minimum": 0
},
"position_premium_liability": {
"description": "The liability resulting from the position's premium.",
"type": "number",
"format": "integer",
"minimum": 0
}
}
}
Upon joining the channel the user will be pushed a list of all active positions such as in the example below:
[
null,
null,
"active_positions:fbNZHhLfj8Ts2jxDczSQrOEBSdg1",
"all_positions",
{
"positions": [
{
"market_id": "687e9cdb-a391-4118-aa80-1122bb14779f",
"position": -70,
"premium": 3500,
"buy_order_liability": 0,
"sell_order_liability": 0,
"position_liability": 2100,
"position_premium_liability": -3500,
"total_liability": 2100,
"market_value": -4900,
"unrealized_pnl": -1400
}
]
}
]
Furthermore, upon updates to the position the user will get a push message such as:
[
null,
null,
"active_positions:cW7UI52lToTDbIEYaXAFtqYYaZB2",
"new_positions",
{
"positions": [
{
"market_id": "687e9cdb-a391-4118-aa80-1122bb14779f",
"position": -70,
"premium": 3500,
"buy_order_liability": 0,
"sell_order_liability": 0,
"position_liability": 2100,
"position_premium_liability": -3500,
"total_liability": 2100,
"market_value": -4900,
"unrealized_pnl": -1400
}
]
}
]
As trades are executed on the account, these updates will be fed to the user.
The Active Orders Channel delivers information to the subscriber on order that are filled against an active market in the system. Specifically these updates occur when a previously unfilled order is filled. For example, if the user were to put in a limit order to buy 10 @ $45 and no liquidity was available at that price, the order would sit in the system waiting to be filled, when someone filled the order, either partially or completely, the update would be sent to the users. To join the channel this command should be issued to the socket with the user's UUID in place of {{UUID}}
:
["3","3","active_orders:{{UUID}}","phx_join", ""]
The Active Orders Channel also has a feature that enables a user to have orders automatically cancelled upon disconnect from the channel, either intentional or by failing to deliver a timely ping. This feature is extremely useful in the situation where order pricing is sensitive and continued connectivity is needed to adjust orders. To enable this feature the user should join in the following manner:
["3","3","active_orders:{{UUID}}","phx_join", {"cancel_on_disconnect": true, "ping_timeout": 100000}]
The cancel_on_disconnect
sets the feature on and the ping timeout is the requested time in milliseconds to permit between pings to allow the socket to still be connected. Note that there are limitations, both lower and upper, on ping timeout so your request for a timeout might not be granted.
If the feature is enabled the server will return payload like the following:
{"cancel_on_disconnect": true, "ping_timeout": 15000}
In this case the user requested a 100000µs
timeout but the server adjusted that to 15000µs
.
If the user connects more than once to the active orders with this feature on, the cancellation of orders will only occur once the last channel fails the ping heartbeat or is manually disconnected.
The schema of properties delivered on the active orders channel is:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/active_order_schema",
"title": "ActiveOrder",
"type": "object",
"description": "Information on an order in the active orders channel",
"properties": {
"id": {
"description": "The unique ID of the order.",
"type": "string",
"format": "uuid"
},
"market_id": {
"description": "The unique id of the market the order was placed on.",
"type": "string",
"format": "uuid"
},
"action": {
"description": "Whether to buy contracts or sell them.",
"type": "string",
"enum": [
"buy",
"sell"
]
},
"price": {
"description": "The base price for the contracts if the order has type=limit.",
"type": [
"number",
"null"
],
"format": "integer",
"minimum": 1,
"maximum": 99
},
"quantity": {
"description": "The amount of contracts to buy or sell.",
"type": "number",
"format": "integer",
"exclusiveMinimum": 0
},
"order_type": {
"description": "The type of order placed against the market.",
"type": "string",
"enum": [
"limit",
"market"
]
},
"status": {
"description": "The current status of the order.",
"type": "string",
"enum": [
"open",
"filled",
"cancelled"
]
},
"avg_price": {
"description": "The average price in cents that the order was traded on.",
"type": [
"number",
"null"
],
"format": "integer",
"minimum": 0
},
"filled_percentage": {
"description": "The integer percentage of the order that has been filled.",
"type": "number",
"format": "integer",
"minimum": 0,
"maximum": 100
},
"time": {
"description": "Time when the order was placed.",
"type": "string",
"format": "datetime"
}
}
}
Note that when the user first connects to the channel the channel will send them a list of all active orders currently in the system which would look something like this if there is only one active order:
[
null,
null,
"active_orders:fbNZHhLfj8Ts2jxDczSQrOEBSdg1",
"all_orders",
{
"orders": [
{
"action": "sell",
"avg_price": null,
"filled": 0,
"filled_percentage": 0,
"id": "7dc6671e-2852-44f4-803d-4405d8a85407",
"market_id": "002b030c-5ac2-41f4-92de-175e666b0a70",
"order_type": "limit",
"price": 10,
"quantity": 20,
"status": "open",
"time": "2020-11-06T21:34:30.376858Z"
}
]
}
]
Like the portfolio channel, active_orders
requires the user to send a heartbeat ping at regular intervals to keep the server from automatically closing. A message like the following can be sent to the channel:
["1", "1", "active_orders:{{UUID}}", "ping", ""]
Trades are sent to this channel when a trade is executed on an order. Note that the order of messages from the Active Orders Channel and Active Trades Channel are not necessarily sequential. They can arrive out of order. Also multiple trades could arrive on the Active Trades Channel for each order update. It is up to the user to manage the incomming data and update relevant local data structures. Also it should be noted that a trade could arrive even after sending a cancel on the order because others may have taken the liquidity before the cancels could be processed.
To join the active trades channel use this command with {{UUID}}
replaced by the user's UUID.
["3","3","active_trades:{{UUID}}","phx_join", ""]
The schema for messages on the channel is as folows.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/active_trade_schema",
"title": "ActiveOrder",
"type": "object",
"description": "Information on a trade in the active trades channel",
"properties": {
"id": {
"description": "The unique ID of the trade",
"type": "string",
"format": "uuid"
},
"market_id": {
"description": "The unique id of the market the order was placed on.",
"type": "string",
"format": "uuid"
},
"order_id": {
"description": "The unique id of the order the trade is assosiated with.",
"type": "string",
"format": "uuid"
},
"action": {
"description": "Whether to buy contracts or sell them.",
"type": "string",
"enum": [
"buy",
"sell"
]
},
"price": {
"description": "The price used to buy or sell the contracts.",
"type": [
"number",
"null"
],
"format": "integer",
"exclusiveMinimum": 0
},
"filled": {
"description": "The amount of contracts to bought or sold.",
"type": "number",
"format": "integer",
"exclusiveMinimum": 0
},
"remaining": {
"description": "The amount of contracts not settled.",
"type": "number",
"format": "integer",
"exclusiveMinimum": 0
},
"premium": {
"description": "The premium paid to execute the trade.",
"type": "number",
"format": "integer"
},
"remaining_premium": {
"description": "The remaining premium after settlements.",
"type": "number",
"format": "integer"
},
"max_potential_fee": {
"description": "The max potential fee on a trade based on settlements and remaining part.",
"type": "number",
"format": "integer"
},
"time": {
"description": "Time when the order was placed.",
"type": "string",
"format": "datetime"
},
"inserted_at": {
"description": "Time when the order was placed.",
"type": "number",
"format": "integer"
}
}
}
After joining the platform will send a user's active trades, which are defined as trades that have not been expired or closed yet. The example message is here:
[
null,
null,
"active_trades:cW7UI52lToTDbIEYaXAFtqYYaZB2",
"all_trades",
{
"trades": [
{
"action": "sell",
"filled": 20,
"id": "787582ec-3863-4760-bbcb-398d3dca8fe5",
"market_id": "687e9cdb-a391-4118-aa80-1122bb14779f",
"premium": 480,
"price": 24,
"time": "2020-11-01T19:20:34.890853Z"
},
{
"action": "buy",
"filled": 20,
"id": "c1c3614c-32ae-4bea-bae4-fc3f52047790",
"market_id": "687e9cdb-a391-4118-aa80-1122bb14779f",
"premium": -420,
"price": 21,
"time": "2020-11-01T01:55:26.857931Z"
}
]
}
]
Other trade updates will be pushed to the channel as trades are executed in the system.
Settlements are generated in the STX Exchange when either a trade occurs that liquidates a position or a market expires. In the first case, envision a user has a position of +10 in a market and decides to sell 5 contracts. When the trade occurs 5 of the existing 10 contracts that a user owns will be sold and a settlement will be generated based on the results of the sale.
To join the channel issue the following command, replacing {{UUID}}
with the user's UUID:
["3","3","active_trades:{{UUID}}","phx_join", ""]
To ping the channel to keep it open the command is simple:
["1", "1", "active_orders:{{UUID}}", "ping", ""]
The schema of attributes for the data pushed to the channel is as follows:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/active_order_schema",
"title": "ActiveOrder",
"type": "object",
"description": "Information on an order in the active orders channel",
"properties": {
"id": {
"description": "The unique ID of the order.",
"type": "string",
"format": "uuid"
},
"market_id": {
"description": "The unique id of the market the order was placed on.",
"type": "string",
"format": "uuid"
},
"action": {
"description": "Whether to buy contracts or sell them.",
"type": "string",
"enum": [
"buy",
"sell"
]
},
"price": {
"description": "The base price for the contracts if the order has type=limit.",
"type": [
"number",
"null"
],
"format": "integer",
"minimum": 1,
"maximum": 99
},
"quantity": {
"description": "The amount of contracts to buy or sell.",
"type": "number",
"format": "integer",
"exclusiveMinimum": 0
},
"order_type": {
"description": "The type of order placed against the market.",
"type": "string",
"enum": [
"limit",
"market"
]
},
"status": {
"description": "The current status of the order.",
"type": "string",
"enum": [
"open",
"filled",
"cancelled"
]
},
"avg_price": {
"description": "The average price in cents that the order was traded on.",
"type": [
"number",
"null"
],
"format": "integer",
"minimum": 0
},
"filled_percentage": {
"description": "The integer percentage of the order that has been filled.",
"type": "number",
"format": "integer",
"minimum": 0,
"maximum": 100
},
"time": {
"description": "Time when the order was placed.",
"type": "string",
"format": "datetime"
}
}
}
An example message of the channel is as follows:
[
null,
null,
"active_settlements:cW7UI52lToTDbIEYaXAFtqYYaZB2",
"new_settlements",
{
"settlements": [
{
"id": "801d773e-fecc-418a-b377-155605595178",
"type": "closed_short",
"opening_trade_id: "17aa760d-50c0-4c1a-a5c6-fe4af4adb4bb",
"closing_trade_id: "7f959093-6a30-4b48-9a59-d1e09f004340",
"opening_price: 10,
"closing_price: 20,
"quantity: 100,
"fee": 50,
"gross_pnl: 1000,
"realized_pnl: 950,
"inserted_at: "2020-11-01T19:02:28.125460Z",
"market_id: "687e9cdb-a391-4118-aa80-1122bb14779f",
"account_id: "c1c3614c-32ae-4bea-bae4-fc3f52047790"
}
]
}
]
Just as with the Active Orders and Active Trades channels, this channel will not necessarily deliver sequential data with the others. It is up to the user to collect the asynchrous information and handle it properly.
If data on a user, such as their limits and active status, changes messages will be sent to the User Info channel. As is the case with other channels this channel must be joined in the following manner with {{UUID}}
replaced with the user's UUID.
["3","3","user_info:{{UUID}}","phx_join", ""]
The schema for updates to the channel is:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://sportsx.com/user_info_channel_schema",
"title": "UserInfo",
"type": "object",
"description": "Information about the user",
"properties": {
"address1": {
"description": "The address line 1 of the user's address.",
"type": "string",
"format": "alphanumeric"
},
"address2": {
"description": "The optional address line 2 of the user's address.",
"type": "string",
"format": "alphanumeric"
},
"city": {
"description": "The city of the user's address.",
"type": "string",
"format": "alpha"
},
"country": {
"description": "The country of the user's address.",
"type": "string",
"format": "alpha"
},
"dateOfBirth": {
"description": "The date of birth of the user.",
"type": "string",
"format": "yyyy-mm-dd"
},
"firstName": {
"description": "The first name of the user.",
"type": "string",
"format": "alpha"
},
"industry": {
"description": "The industry in which the user is employed.",
"type": "string",
"format": "alpha"
},
"jobTitle": {
"description": "The job title of the user.",
"type": "string",
"format": "alphanumeric"
},
"lastName": {
"description": "The last name of the user.",
"type": "string",
"format": "alpha"
},
"optInMarketing": {
"description": "If the user has opted-in for marketing information from STX",
"type": "boolean",
"format": "true or false"
},
"passwordChanged": {
"description": "If the user has changed the password or not. It will send true as soon as the user changed the password",
"type": "boolean",
"format": "true or false"
},
"phoneNumber": {
"description": "The phone number of the user.",
"type": "string",
"format": "number"
},
"pictureUrl": {
"description": "The url of the uploaded picture of the user.",
"type": "string",
"format": "alphanumeric"
},
"state": {
"description": "The state in the address of the user.",
"type": "string",
"format": "alpha"
},
"testAccount": {
"description": "If this user's account is marked as test account.",
"type": "boolean",
"format": "true or false"
},
"userId": {
"description": "The ID of the user",
"type": "string",
"format": "uuid"
},
"userUid": {
"description": "The external user id of the user.",
"type": "string",
"format": "alphanumeric"
},
"userStatus": {
"description": "The status of the user.",
"type": "string",
"enum": [
"archived",
"pending_approval",
"active",
"suspended",
"banned",
"self_excluded",
"closed",
"rejected",
"dormant"
]
},
"username": {
"description": "The generated username of the user.",
"type": "string",
"format": "alpha"
},
"zipCode": {
"description": "The zip code in the address of the user.",
"type": "string",
"format": "number"
}
}
}
When an user's information changes an update is pushed to the channel in a message such as the following example.
[[
null,
null,
"user_info:v5yDoWte75hCuhp3ejRMPTWz8Yz1",
"user_updated",
{
"address1": "123 Main St",
"address2": null,
"city": "New York",
"country": "USA",
"dateOfBirth": "1988-03-05",
"firstName": "Jonny",
"industry": null,
"jobTitle": null,
"lastName": "Lu",
"optInMarketing": true,
"password_changed": false,
"phoneNumber": null,
"pictureUrl": null,
"state": "NY",
"test_account": false,
"userId": "7fb55544-3a7e-4f6b-bd66-fc89bd3a1087",
"userStatus": "active",
"userUid": "v5yDoWte75hCuhp3ejRMPTWz8Yz1",
"username": "Lu.Jonny.47541",
"zipCode": "10121"
}
]
This is an optional feature enabled when joining the channel by sending a payload with keys:
cancel_on_disconnect
set to trueping_timeout
set to milliseconds telling the server when to consider the channel deadEnabling this feature has to go hand in hand with the cancelOnDisconnect
flag when placingorders via GraphQL confirmOrder
mutation.
When the feature is enabled, the server will expect the client to send ping
messages to the active_orders
channel. If ping is not received within the set timeout, the server will consider the channel dead and will start cancelling orders flagged for cancellations that are older than the set ping_timeout
.
Every ping
message sent to the channel resets timeout
It may happen that an order with the flag cancelOnDisconnect
is created after the last ping
message
was sent. In this case, the order will be cancelled when the last set ping_timeout
expires.
This also means that if flagged orders are placed after the channel is considered dead, such orders will be cancelled after the last ping_timeout
set.
In this case, the server will assume that the timeout is at the top of the allowed range (20s, depending on the server's configuration).
active_orders
channelA user can join the channel from multiple sockets and enable the feature on more than one channel. In this case, the order cancellation kicks in only after all the channels are considered dead.
Then, the flagged orders will be cancelled after the server detects the last ping expired. This gives you some time to reconnect if you don't want to automatically cancel the flagged orders.
["3","3","active_orders:{{UID}}","phx_join", ""]
["3","3","active_orders:{{UUID}}","phx_join", {"cancel_on_disconnect": true, "ping_timeout": 10000}]
If the feature is enabled the server will return payload like the following:
{"cancel_on_disconnect": true, "ping_timeout": 15000}
NOTE: ping_timeout
unit is milliseconds
NOTE: The server may set different ping_timeout
when requested when enabling the feature.
This happens when provided value is out of allowed range.
["1", "1", "active_orders:{{UUID}}", "ping", ""]
STX maintains a high standard of customer service when assisting our integrators. Unlike other operators we want third parties to integrate with our server. In addition to contacting STX by email, we would be happy to create a slack channel where you can ask questions, get clarifications and even request features of our engineers. You wont be talking to some customer support center but the actual STX engineering team. Please contact us for further support and to set up a channel.