Unity Integration
Gx402 shows how to integrate x402 payments into Unity projects using engine-native UI and wallet connectors (WalletConnect, browser wallets for WebGL). We provide example Unity scripts, a test scene, and an Express middleware example to handle server-side verification and x402 calls.Prerequisites
- Unity 2020.3 LTS or newer (2021+ recommended)
High-Level Flow
- EVM
- Solana
EVM
- Connect Wallet
Gx402Managertriggers EVM wallet connect.- User approves in MetaMask/Rabby.
- Address is saved for onchain verification.
- Initiate Payment Request
- Player taps Buy / Call API.
- Unity triggers
HandleApiCallOptimized(). - Validates EVM address + fetches chain ID.
- Build the Transaction
- Unity prepares an ERC-20 USDC transfer:
- Resolve token contract for the chain.
- Convert amount → token units (decimals).
- ABI-encode transfer(recipient, amount).
- Estimate gas + set to, data, value=0.
-
Sign and Send
- Wallet pops up with transaction details.
- On approval, Unity sends the tx.
- Network returns a tx hash (0x…).
- Unity waits for receipt (status == 1).
-
Verify and Unlock (x402 Flow)
- Unity sends the txHash in the X-402-Payment header.
- Backend checks ERC-20 Transfer event for correct from → to → amount.
- On success, backend returns 200 OK.
- Unity displays: ✅ “Payment Verified — Access Granted!”
Solana
- Connect Wallet
The player taps Connect Wallet in the Unity UI.Gx402ManagercallsConnectWalletAsync()from the Solana SDK (e.g., MagicBlocks).- The user approves the prompt in Phantom or Solflare.
- Once connected,
Web3.Accountstores the wallet address and updates the UI with the active wallet.
- Initiate Payment Request
The player taps Buy (or Call API) to begin an x402-protected flow.- Unity triggers
HandleApiCallOptimized(). - The script validates the connected wallet (
CheckWallet()). - The payer’s public key is retrieved to identify who’s making the payment.
- Unity triggers
- Build the Transaction
Unity constructs a Solana USDC transaction viaBuildUsdcTransaction().- Fetches a recent blockhash for transaction validity.
- Derives payer and recipient token accounts (ATAs).
- Bundles three key instructions:
- Create payer ATA (idempotent)
- Create recipient ATA (idempotent)
- Transfer required USDC amount
- Finalizes and signs the transaction object with the payer as the fee payer.
- Sign and Send
- The wallet prompts the player to approve and sign the transaction.
- On approval, Unity sends it using
SendTransactionAsync(). - The Solana network returns a transaction signature (
TX ID), confirming the payment on-chain. ConfirmTransaction()ensures final settlement before proceeding.
- Verify and Unlock (x402 Flow)
- Unity sends the transaction signature in the
X-402-Paymentheader via a POST request to your backend. - The backend verifies the transaction (correct amount, mint, recipient).
- Once confirmed, it unlocks the API endpoint or in-game reward.
- Unity receives a
200 OKresponse and updates the UI to show:
✅ “Payment Verified — Access Granted!”
- Unity sends the transaction signature in the
Unity WalletConnection (C#)
Copy
// Unity - simple async flow (concept)
{
Web3.OnWalletChangeState += OnWalletStateChanged;
OnWalletStateChanged();
}
private void OnDestroy()
{
Web3.OnWalletChangeState -= OnWalletStateChanged;
}
public void ConnectWallet()
{
ConnectWalletAsync().Forget();
}
public void DisconnectWallet()
{
if (Web3.Instance != null && Web3.Wallet != null)
{
Web3.Instance.Logout();
}
}
private async UniTask ConnectWalletAsync()
{
if (Web3.Instance == null)
{
Debug.LogError("Web3.Instance is not found.");
UpdateStatus("Error: Web3 not initialized.");
return;
}
try
{
UpdateStatus("Connecting...");
Account connectedAccount = await Web3.Instance.LoginWalletAdapter();
if (connectedAccount == null)
{
UpdateStatus("Connection cancelled.");
}
}
catch (Exception e)
{
Debug.LogError($"Error connecting wallet: {e.Message}");
UpdateStatus("Connection failed.");
}
}
Unity CallApiButton (C#)
Now to call the API with Gx402 you just need to call HandleApi function from gx402 game managerCopy
// Unity - simple async flow (concept)
public void CallApiButton()
{
HandleApiCallOptimized().Forget();
}
private async UniTask HandleApiCallOptimized()
{
if (!CheckWallet()) return;
try
{
UpdateStatus("Preparing transaction...");
PublicKey payerPublicKey = Web3.Account.PublicKey;
var transaction = await BuildUsdcTransaction(
payerPublicKey,
new PublicKey(RECIPIENT_OWNER_ADDRESS),
new PublicKey(USDC_MINT_DEVNET),
AMOUNT_REQUIRED,
TOKEN_DECIMALS
);
if (transaction == null)
{
UpdateStatus("Error: Could not build transaction.");
return;
}
UpdateStatus("Please sign transaction...");
var signedTx = await Web3.Wallet.SignTransaction(transaction);
if (signedTx == null)
{
UpdateStatus("Error: Transaction signing failed.");
return;
}
UpdateStatus("Sending transaction...");
var sendResult = await Web3.Rpc.SendTransactionAsync(signedTx.Serialize());
string signature = sendResult.WasSuccessful ? sendResult.Result : null;
if (string.IsNullOrEmpty(signature))
{
UpdateStatus("Error: Failed to send transaction.");
Debug.LogError($"[X402] Failed to send transaction: {sendResult.Reason}");
return;
}
await Web3.Rpc.ConfirmTransaction(signature, Commitment.Confirmed);
Debug.Log($"[X402] Transaction confirmed! Signature: {signature}");
await PostWithSignature(signature, new RequestBody { someData = "hello from Unity" });
}
catch (Exception e)
{
Debug.LogError($"[X402] Optimized Flow Error: {e.Message}");
UpdateStatus($"Error: {e.Message}");
}
}
Express Server
Copy
// app/api/try/route.ts (or pages/api/try.ts)
import { NextRequest, NextResponse } from 'next/server';
import { X402PaymentHandler } from 'x402-solana/server';
// ✅ Define CORS headers ONCE — include X-402-Payment!
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-402-Payment',
};
const x402 = new X402PaymentHandler({
network: 'solana-devnet',
treasuryAddress: process.env.TREASURY_WALLET_ADDRESS!,
facilitatorUrl: 'https://facilitator.payai.network', // ✅ No trailing spaces!
});
export async function OPTIONS() {
// ✅ Return full CORS headers in preflight
return new NextResponse(null, { status: 204, headers: corsHeaders });
}
export async function GET(req: NextRequest){
try {
const paymentHeader = x402.extractPayment(req.headers);
const paymentRequirements = await x402.createPaymentRequirements({
price: {
amount: "2500000",
asset: {
address: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr",
decimals: 6,
},
},
network: 'solana-devnet',
config: {
description: 'AI Chat Request Example',
resource: `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:61664'}/api/try` as `${string}://${string}`,
},
});
if (!paymentHeader) {
const response = x402.create402Response(paymentRequirements);
return NextResponse.json(response.body, {
status: response.status,
headers: corsHeaders, // ✅ Include CORS in 402 too
});
}
const verified = await x402.verifyPayment(paymentHeader, paymentRequirements);
if (!verified) {
return NextResponse.json(
{ error: 'Invalid or unverified payment' },
{ status: 402, headers: corsHeaders }
);
}
const body = await req.json().catch(() => ({}));
const result = {
message: '✅ Hello, you paid successfully!',
receivedData: body,
};
await x402.settlePayment(paymentHeader, paymentRequirements);
return NextResponse.json(result, { headers: corsHeaders });
} catch (error: any) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500, headers: corsHeaders }
);
}
}
export async function POST(req: NextRequest) {
try {
const paymentHeader = x402.extractPayment(req.headers);
const paymentRequirements = await x402.createPaymentRequirements({
price: {
amount: "2500000",
asset: {
address: "abcmasdmajwindojassmdjoaskmdass",
decimals: 6,
},
},
network: 'solana-devnet',
config: {
description: 'Request Example',
resource: `${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:61664'}/api/try` as `${string}://${string}`,
},
});
if (!paymentHeader) {
const response = x402.create402Response(paymentRequirements);
return NextResponse.json(response.body, {
status: response.status,
headers: corsHeaders, // ✅ Include CORS in 402 too
});
}
const verified = await x402.verifyPayment(paymentHeader, paymentRequirements);
if (!verified) {
return NextResponse.json(
{ error: 'Invalid or unverified payment' },
{ status: 402, headers: corsHeaders }
);
}
const body = await req.json().catch(() => ({}));
const result = {
message: '✅ Hello, you paid successfully!',
receivedData: body,
};
await x402.settlePayment(paymentHeader, paymentRequirements);
return NextResponse.json(result, { headers: corsHeaders });
} catch (error: any) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500, headers: corsHeaders }
);
}
}
