Skip to main content

Primary Sales

A primary sale involves a game studio selling an NFT directly to a player in exchange for an ERC20 or Native token. The NFT can either be transferred from the game studio's inventory or directly minted to the player's wallet during the process.

Immutable offers the foundational elements for game studios to establish their own primary sales storefront.

Immutable's primary sales feature offers several advantages for game studios:

  1. Balance Lock: This feature allows players' ERC20 or Native tokens required for a primary sale to be temporarily held in their wallet while the request is processed. If the sale does not proceed, the hold can be released. This ensures a smoother customer experience compared to directly transferring ERC20 or Native tokens to a game studio wallet address.

  2. Reconciliation IDs: Immutable's primary sales primitives allow game studios to define studio_data during a primary sale, while the platform generates a unique primarySaleID for each request. These fields act as identifiers, linking primary sales requests with buyers' ERC20 or Native token transfers and NFT mints/transfers. They play a crucial role in auditing and reconciliation processes.

📝Guides

Core SDK

1. Prerequisites

1.1 Storefront and Backend Application

Ensure your game has a fully functional storefront and a backend application for inventory management and transaction control. Your storefront should comply with the terms and conditions of your game distributor/launcher.

1.2 Minting Backend Application

Prepare a backend application capable of minting NFTs and ready to integrate with webhook (SNS) events.

1.3 Core SDK Package

To utilize the Core SDK, initialize it in both the frontend and backend of your storefront. Make sure you are using version 2.5.4 or higher of the core-sdk.

1.4 Programmatic Signer Wallet (Hot Wallet)

Set up a programmatic signer wallet (hot wallet) for signing payloads from your backend when accepting or rejecting primary sales. It's advisable to use a separate, more secure cold wallet for receiving payments and fees. Ensure that the cold wallet for fee recipients is registered with Immutable X following the same wallet initialization process to assign a stark key to the wallet.

1.5 Webhooks

Implement webhooks to receive notifications about events related to primary sales. It's highly recommended to verify the authenticity of messages received via webhooks. You can use methods like signature and sender verification to ensure message integrity. Please see Webhooks | Signature and Sender Verification for more information. AWS | JS SNS Message Validator may help if your backend is based on JavaScript.

2. Creating a Primary Sale Request

Initiating a primary sale begins when a player selects the desired asset in your storefront, prompting your application to notify Immutable of the primary sale request.

Sending the request will start the balance lock process which will hold the buyer's funds whilst your application process the primary sales request.

Below is an example of such a request:

ParameterDescription
ENVIRONMENTEnvironment the primary sales is occurring in: SANDBOX (Testnet) or PRODUCTION (Mainnet)
PAYMENT_ERC20_AMOUNTQuantity of ERC20 tokens required for the principal payment to purchase the NFT through a primary sale (excluding fees)
PAYMENT_ERC20_ADDRESSContract address of the ERC20 contract needed for purchasing the NFT through a primary sale
ERC20_DECIMALSNumber of decimal places supported by the ERC20 token.
PAYMENT_RECIPIENT_ETHER_KEYAddress collecting the principal payment (excluding fees) for the primary sale
STUDIO_ETHER_KEYAddress used to authenticate requests for accept/reject from the studio
STUDIO_DATAFree text field for linking a primary sale from Immutable X with the studio's inventory. This can be an identifier issued by the studio’s backend, preferably a signature of the identifier issued by the backend to ensure buyer-initiated purchases.
FEEAdditional ecosystem fee, in basis points, that may be charged for the primary sale.
Immutable's protocol fee of 2% is automatically applied and doesn't need to be included here. Up to 2 additional ecosystem fees can be included, but the total fee amounts must not exceed the primary sale amount.
These fees are calculated on top of the purchase price.
For example, if the listed price for an item is 100 IMX and there's an additional ecosystem fee of 1%, the total amount the user pays will be 103 IMX (i.e., 100 + 2 + 1).
FEE_COLLECTION_ADDRESSWallet collecting the primary sale fee if charged

const config = Config.ENVIRONMENT;
const imxClient = new ImmutableX(config);

// this will usually be triggerd by a button click etc
imxClient.createPrimarySale(walletConnection, {
body: {
buyer_ether_key: (await walletConnection.ethSigner.getAddress()),
payment_amount: "PAYMENT_ERC20_AMOUNT",
payment_token: {
type: "ERC20",
data: {
// see https://docs.immutable.com/docs/x/token-data-object/
token_address: "PAYMENT_ERC20_ADDRESS",
decimals: ERC20_DECIMALS
}
},
// ISO string of a future time
expiration_timestamp: time,
items_recipient_ether_key: (await walletConnection.ethSigner.getAddress()),
payment_recipient_ether_key: "PAYMENT_RECIPIENT_ETHER_KEY",
studio_data: "STUDIO_DATA",
studio_ether_key: "STUDIO_ETHER_KEY",
fees: [
{
fee_percentage: FEE // basis point,
address: "FEE_COLLECTION_ADDRESS"
}
]

}
});


tip

Your storefront should conduct several basic checks to ensure the successful creation of a primary sale:

  1. User Registration: Verify that the user is registered on Immutable X before proceeding with the primary sale.
  2. Sufficient Balance: Ensure that the user has a sufficient balance in the selected token to complete the purchase.

After the createPrimarySale call is made successfully, you will receive a payload for the primary sale in a PENDING state. See the options listed below for recieving this payload via webhooks (recommned) or polling.

This indicates that Immutable X is asynchronously performing a balance lock on the user's balance. If the balance locking process succeeds, the primary sale will transition to the ACTIVE state.

In case the balance lock fails, the primary sale will transition to the INVALID state, indicating that the user's funds have not been locked and the purchase is unsuccessful. Your storefront should revalidate the balance and retry the creation process if appropriate.

Option 1: Use webhooks to monitor for primary sales events

Webhooks can be used to monitor the status of primary sales requests.

Your application will be notified of the change of status of a primary sales by listening for the primary_sale_updated webhook events and filtering for events in the ACTIVE state to perform the next action.

Here is an example of a primary_sale_updated

{
"event_id": "018e4fad-77a0-69da-daf4-aa901a7c36bc",
"creation_time": "2024-03-18 03:49:12.208438",
"reference_id": "25",
"body": {
"id": 25,
"fees": [
{
"type": "ECOSYSTEM",
"amount": "1000000000",
"address": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"percentage": 100
},
{
"type": "PROTOCOL",
"amount": "2000000000",
"address": "0x52d79b559b3b8cae669444f744bea8b27a11be11",
"percentage": 200
}
],
"status": "ACTIVE",
"created_at": "2024-03-18T03:49:09.649827Z",
"expires_at": "2024-03-18T05:00:00Z",
"updated_at": "2024-03-18T03:49:12.208935Z",
"studio_data": "test1",
"payment_token": {
"data": {
"decimals": 18,
"token_address": "0x5b26a75ee4a4b68a8fe8f94e4b729ff1b8a31051"
},
"type": "ERC20"
},
"payment_amount": "100000000000",
"buyer_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"studio_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"items_recipient_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"payment_recipient_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d"
},
"context": {
"trace_id": "69f888fd75d6633bb75008bfbfb32e6a",
"span_id": "43889d4f1e6cb8bd"
}
}

Option 2: Polling to monitor for primary sales events

Alternatively, polling the API can be used to check the status of primary sales. This endpoint can be used on the frontend to provide immediate feedback to the user about the successful creation of the primary sale.

Here is an example of the response returned by using the GetPrimarySale endpoint.

{
"result": {
"buyer_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"created_at": "2024-03-18T03:49:10.67859Z",
"expires_at": "2024-03-18T05:00:00Z",
"fees": [
{
"address": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"amount": "1000000000",
"percentage": 100,
"type": "ECOSYSTEM"
},
{
"address": "0x52d79b559b3b8cae669444f744bea8b27a11be11",
"amount": "2000000000",
"percentage": 200,
"type": "PROTOCOL"
}
],
"id": 26,
"items_recipient_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"payment_amount": "100000000000",
"payment_recipient_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"payment_token": {
"data": {
"decimals": 18,
"token_address": "0x5b26a75ee4a4b68a8fe8f94e4b729ff1b8a31051"
},
"type": "ERC20"
},
"status": "PENDING",
"studio_data": "test1",
"studio_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"updated_at": "2024-03-18T03:49:10.67859Z"
}
}

3. Accept a primary sale

Once the primary sale request transitions to an ACTIVE state (indicating a successful balance lock by Immutable), your application can proceed with accepting the primary sale.

Below is a code snippet demonstrating how a game studio can accept a primary sale request using the primarySaleID obtained from the primary_sale_updated payload in the previous step:

ParamDescription
PRIMARY_SALE_IDThe unique ID of the primary sale, generated by Immutable once the primary sales request has been acknowledged
const primarySaleID = 'PRIMARY_SALE_ID';
const res = await imxClient.acceptPrimarySale(ethSigner, primarySaleID);
console.log(res);

The ethSigner employed here represents an ethers.AbstractSigner, which can be instantiated using a server-side private key secret or a programmatic signer via a service like AWS KMS.

Upon successful execution of the acceptPrimarySale request, a response containing the primary sale entity is provided, accompanied by another primary_sale_updated event.

Here is an example of this response:

{
"result": {
"buyer_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"created_at": "2024-03-18T03:49:10.67859Z",
"expires_at": "2024-03-18T05:00:00Z",
"fees": [
{
"address": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"amount": "1000000000",
"percentage": 100,
"type": "ECOSYSTEM"
},
{
"address": "0x52d79b559b3b8cae669444f744bea8b27a11be11",
"amount": "2000000000",
"percentage": 200,
"type": "PROTOCOL"
}
],
"id": 26,
"items_recipient_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"payment_amount": "100000000000",
"payment_recipient_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"payment_token": {
"data": {
"decimals": 18,
"token_address": "0x5b26a75ee4a4b68a8fe8f94e4b729ff1b8a31051"
},
"type": "ERC20"
},
"status": "IN_PROGRESS",
"studio_data": "test1",
"studio_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"updated_at": "2024-03-18T03:51:04.819212Z"
}
}

The primary sale status will progress to an IN_PROGRESS state, indicative of the ongoing payment and fee transfer process. This can be queried using webhooks or polling, similar to the previous step.

Upon completion of the payment transfer, the primary sale will transition to an ACCEPTED state, signaling the conclusion of the payment phase. Your application is now ready to proceed with the minting or transferring of assets associated with the primary sale for the user.

In the event of a payment failure, the primary sale transitions to the FAILED status. If this occurs, the balance lock of the buyer's funds will be released, and they must resubmit the primary sales request. While such occurrences are rare, they could potentially stem from users initiating a force-withdrawal of their tokens through alternative contracts. It is important that your application accommodates this possibility.

4. Reject a primary sale

Occasionally, you may need to reject a primary sale after the request has been made but before your application accepts the request.

There are several reasons why a primary sale may need to be rejected by your application, including:

  • The asset has already been sold.
  • The buyer is not authorized to buy the requested asset.
  • A backend failure in the storefront necessitates canceling the sale.

Rejecting a primary sale prevents your users' funds from being locked up indefinitely until the sale expires, emphasizing the importance of setting a short expiration period.

Below is a code snippet demonstrating the rejection process using the primary sale ID obtained from the primary_sale_updated payload from step #2:

ParamDescription
PRIMARY_SALE_IDThe unique ID of the primary sale, generated by Immutable once the primary sales request has been acknowledged
const primarySaleID = 'PRIMARY_SALE_ID';
const res = await imxClient.reject(ethSigner, primarySaleID);
console.log(res);

If the rejection request is successful, the primary sale will transition to the REJECTED status. The balance lock applied to the buyer's ERC20 tokens for the transaction will be released.

It is the responsibility of your storefront to notify the user accordingly. Immutable X will refund any locked-up balance for payment back to the user.

5. Expiry of Primary Sale

Setting a brief expiration time for primary sales ensures timely fund return if a sale isn't completed within the expected window.

The expiry occurs at the backend and affects only ACTIVE primary sales, generating a primary_sale_updated event with EXPIRED status.

Upon expiry, the balance lock on the buyer's ERC20 funds is released, reverting the transaction.

Below is an example of an expired event:

{
"result": {
"buyer_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"created_at": "2024-03-01T02:30:56.158042Z",
"expires_at": "2024-03-01T03:00:00Z",
"fees": [
{
"address": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"amount": "100000000000",
"percentage": 100,
"type": "ECOSYSTEM"
},
{
"address": "0x52d79b559b3b8cae669444f744bea8b27a11be11",
"amount": "200000000000",
"percentage": 200,
"type": "PROTOCOL"
}
],
"id": 24,
"items_recipient_ether_key": "0x3e290fe8f2a5db60a81cb47ea296e0299048dd71",
"payment_amount": "10000000000000",
"payment_recipient_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"payment_token": {
"data": {
"decimals": 18,
"token_address": "0x5b26a75ee4a4b68a8fe8f94e4b729ff1b8a31051"
},
"type": "ERC20"
},
"status": "EXPIRED",
"studio_data": "test1",
"studio_ether_key": "0xc606830d8341bc9f5f5dd7615e9313d2655b505d",
"updated_at": "2024-03-01T03:00:00.041288Z"
}
}