This guide covers the practical path from a fresh Unity project to a functioning in-game economy using the Linq SDK. You'll provision wallets for authenticated players, handle currency balances, initiate purchases, and process payout requests — with annotated code samples that address the async patterns Unity developers actually run into.
Prerequisites: Unity 2022 LTS or later, .NET Standard 2.1 compatible scripting runtime, a Linq developer account with an API key (available via our developer portal), and basic familiarity with Unity's coroutine and async/await patterns. The Linq SDK uses async/await throughout — if your project is still on coroutine-only patterns, you'll want to use the provided LinqAsync.ToCoroutine() helper wrapper.
Package Installation
The Linq SDK is distributed via Unity Package Manager (UPM) using a git URL. In the Unity Editor, open Window > Package Manager, click the + button, and select Add package from git URL. Enter:
https://github.com/linq-games/linq-unity-sdk.git#v1.4.2
Pin to a specific version tag rather than tracking the default branch in production builds — this ensures your build pipeline is deterministic. The package installs the core SDK, the Linq namespace, and the editor configuration window. You can verify the installation by checking for Linq.SDK in your project's Packages/ directory.
The SDK has no native plugin dependencies on supported platforms (iOS, Android, standalone PC/Mac/Linux). It uses Unity's UnityWebRequest internally for all HTTP communication, which means it works without additional network permission configurations on most platforms. On Android, ensure your AndroidManifest.xml includes <uses-permission android:name="android.permission.INTERNET" /> — this is present by default in most Unity Android builds but occasionally stripped by manifest merging rules.
SDK Initialization and Configuration
Create a LinqConfig ScriptableObject via the Linq menu in the editor (Linq > Create Config Asset). Assign your API key and environment setting (Sandbox for development, Production for live builds) in the inspector. Store this asset at Resources/LinqConfig.asset — the SDK's initialization call looks for it there by default, though you can pass a path override.
Initialize the SDK once at application startup, before any wallet operations:
using Linq.SDK;
public class GameManager : MonoBehaviour
{
private void Awake()
{
// Loads config from Resources/LinqConfig.asset
var result = LinqSDK.Initialize();
if (!result.Success)
{
Debug.LogError($"[Linq] Init failed: {result.ErrorCode} — {result.Message}");
// Handle gracefully: disable payment UI, log to your error reporting
}
}
}
Initialization is synchronous and fast (it reads the config asset and sets up the internal HTTP client). Do not call any wallet or currency APIs before Initialize() completes — they will throw LinqNotInitializedException if called out of sequence.
Player Authentication and Wallet Provisioning
The Linq SDK operates on a player identity model: each player in your game is represented by a playerId that you supply from your own authentication system. Linq does not replace your authentication layer — it attaches wallet functionality to the player identities you already manage. This keeps the identity source of truth in your system and Linq as a financial infrastructure layer on top of it.
After your player authenticates (however you handle auth — PlayFab, GameSparks, custom auth, whatever your backend runs), call WalletAPI.GetOrCreateAsync to either retrieve the player's existing wallet or provision a new one:
using Linq.SDK;
using Linq.SDK.Wallet;
public class WalletService : MonoBehaviour
{
private PlayerWallet _wallet;
public async Task InitializePlayerWalletAsync(string playerId, string authToken)
{
try
{
var request = new WalletRequest
{
PlayerId = playerId,
AuthToken = authToken, // Your backend auth token, verified server-side
CurrencyCode = "GEMS_V1", // Your defined currency code
DisplayName = "Gems"
};
_wallet = await LinqSDK.Wallet.GetOrCreateAsync(request);
Debug.Log($"[Linq] Wallet ready. Balance: {_wallet.Balance} {_wallet.CurrencyCode}");
}
catch (LinqApiException ex)
{
Debug.LogError($"[Linq] Wallet init error: {ex.StatusCode} — {ex.Message}");
}
}
}
GetOrCreateAsync is idempotent — calling it for a player who already has a wallet returns the existing wallet. Calling it for a new player provisions a wallet with a zero balance and returns it. The AuthToken field is verified server-side by Linq against your configured backend auth endpoint, which you register in the Linq developer portal during setup. This prevents client-side code from provisioning wallets for arbitrary player IDs.
Currency Purchases: The Purchase Flow
IAP flow in the Linq SDK is a two-phase process that mirrors the quote-and-confirm pattern for currency operations. The SDK does not handle payment instrument collection directly — that happens through the app store billing system (StoreKit on iOS, Google Play Billing on Android) or your own web shop checkout. What Linq handles is the credit of purchased currency to the wallet once payment is confirmed.
After your app store purchase completes and you have a verified receipt, call PurchaseAPI.CreditFromReceiptAsync:
// Called in your IAP purchase success callback
public async Task HandleIAPSuccessAsync(string playerId, string storeReceipt,
string productId, string storePlatform)
{
try
{
var creditRequest = new IAPCreditRequest
{
PlayerId = playerId,
StoreReceipt = storeReceipt,
ProductId = productId, // e.g. "com.yourstudio.gems_500"
Platform = storePlatform, // "ios" | "android" | "steam"
ExpectedCurrencyCode = "GEMS_V1",
ExpectedAmount = 500 // Validate client-side before sending
};
var result = await LinqSDK.Purchase.CreditFromReceiptAsync(creditRequest);
_wallet = result.UpdatedWallet;
// Update UI
OnWalletUpdated?.Invoke(_wallet.Balance);
}
catch (LinqDuplicateTransactionException)
{
// Receipt already processed — safe to ignore, wallet balance is current
Debug.LogWarning("[Linq] Duplicate receipt submitted — ignoring.");
}
catch (LinqApiException ex)
{
Debug.LogError($"[Linq] Credit failed: {ex.StatusCode}");
// Surface error to player: "Purchase processing — your currency will be available shortly."
}
}
The LinqDuplicateTransactionException case is important and worth handling explicitly. If your network request times out and the client retries, the receipt may be submitted twice. Linq's receipt processing is idempotent — the second submission is rejected with this exception, the balance is not double-credited. Catch it and continue rather than surfacing an error to the player.
In-Game Purchases: Spending Currency
Spending currency against your item catalog uses TransactionAPI.SpendAsync. Always resolve the item definition from your catalog before calling spend — confirm the cost, confirm the item is available, and confirm the player has sufficient balance. Do not rely on the API to catch balance issues as your primary UX guard — your UI should prevent the call entirely if the balance is insufficient, showing the appropriate deficit screen instead.
public async Task PurchaseItemAsync(string playerId, string itemId, int cost)
{
if (_wallet.Balance < cost)
{
// Don't call the API — surface the deficit UX instead
OnInsufficientFunds?.Invoke(cost - _wallet.Balance);
return;
}
try
{
var spendRequest = new SpendRequest
{
PlayerId = playerId,
Amount = cost,
CurrencyCode = "GEMS_V1",
ReferenceId = itemId,
Metadata = new Dictionary<string, string> {
{ "item_id", itemId },
{ "session_id", GameSession.CurrentId }
}
};
var result = await LinqSDK.Transaction.SpendAsync(spendRequest);
_wallet = result.UpdatedWallet;
// Item delivery is your responsibility — Linq credits/debits currency only
DeliverItemToPlayer(playerId, itemId);
OnWalletUpdated?.Invoke(_wallet.Balance);
}
catch (LinqInsufficientFundsException)
{
// Race condition: balance changed between UI check and API call
OnInsufficientFunds?.Invoke(0);
}
}
Handling Async Responses in MonoBehaviour
The most common Unity-specific issue with the Linq SDK is calling async methods from MonoBehaviour lifecycle events without proper error handling. A failed API call that throws an unhandled exception in a fire-and-forget async method will silently fail — the exception goes to the thread pool, not Unity's main thread, and your UnhandledException handler may not catch it.
For any Linq SDK call that you don't await directly from a button handler or a coroutine, use the provided extension method that wraps the async task with an explicit failure callback:
// Instead of this (unsafe — exceptions disappear):
LinqSDK.Wallet.GetOrCreateAsync(request).ContinueWith(_ => { });
// Use this pattern:
LinqSDK.Wallet.GetOrCreateAsync(request)
.HandleExceptions(ex =>
{
Debug.LogError($"[Linq] Unhandled: {ex.Message}");
Analytics.LogError("linq_wallet_failure", ex);
});
If you're still working in coroutines, the SDK ships with a wrapper:
StartCoroutine(
LinqAsync.ToCoroutine(
LinqSDK.Wallet.GetOrCreateAsync(request),
onSuccess: wallet => { _wallet = wallet; },
onError: ex => { Debug.LogError(ex.Message); }
)
);
Webhook Configuration for Payout Events
If your game includes creator earnings or prize payouts, you'll receive payout lifecycle events via webhook rather than polling. Configure your webhook endpoint in the Linq developer portal. Your server-side endpoint receives POST requests with signed payloads:
// Verify webhook signature before processing
POST /linq-webhooks
Headers:
X-Linq-Signature: sha256=<hmac_of_payload>
X-Linq-Timestamp: <unix_timestamp>
Body:
{
"event_type": "payout.completed",
"payout_id": "pout_9x7a...",
"player_id": "usr_3f8b...",
"amount_usd_cents": 4800,
"method": "ach",
"settled_at": "2026-01-08T10:14:22Z"
}
Always verify the HMAC signature before acting on a webhook payload. The signature is computed over the raw request body using your webhook secret (available in the portal). Reject any request where the timestamp is more than 300 seconds old — this prevents replay attacks.
The SDK integration is straightforward once you have the async patterns right and the auth flow set up. The areas where studios lose time are: not pinning the SDK version in package.json (causes build inconsistencies), not handling LinqDuplicateTransactionException in the receipt flow (causes confusing errors on retry), and not verifying webhook signatures (security issue). Cover those three and the integration is stable.