Payments
Guide to implementing USDC, SOL, and SPL token transfers via Payment intents.
When to Use Payment Intents
Payment intents are for transferring value on Solana — P2P payments, merchant checkout, tips, and any direct asset transfer. They use an SDK-fixed schema with Solana mainnet as the exit target.
Payment is the only intent type with a hardcoded destination and schema. This is intentional and load-bearing: it's what makes "payment" mean something specific rather than becoming "action with a nonce."
Data Payload
pub struct PaymentPayload {
pub amount: u64, // lamports or smallest token unit
pub asset: Vec<u8>, // Mint account address (32 bytes)
pub recipient: Vec<u8>, // Recipient wallet address (32 bytes)
}
amount: Value in the smallest unit of the asset (lamports for SOL, raw units for SPL tokens)asset: The SPL token mint address (32-byte public key). UsePubkey::default()for native SOLrecipient: Destination wallet address (32-byte public key)
Creating a Payment Intent
Expo SDK
import { createPaymentIntent } from '@zypp-labs/expo-sdk';
const amount = 1_000_000; // 0.001 SOL in lamports
const mint = 'So11111111111111111111111111111111111111112'; // wSOL mint
const recipient = 'RecipientPublicKeyHere...';
const { intentId } = createPaymentIntent(amount, mint, recipient);
Unity SDK
var intent = ZyppSdk.CreatePaymentIntent(1_000_000, mintHex, recipientHex);
Rust Core
use zypp_labs_core::types::{PaymentPayload, Intent, IntentType, IntentPayload, TrustTier};
let payment = Intent::new(
uuid::Uuid::new_v4(),
IntentType::Payment,
IntentPayload::Payment(PaymentPayload {
amount: 1_000_000,
asset: wsol_mint.to_bytes().to_vec(),
recipient: recipient.to_bytes().to_vec(),
}),
TrustTier::Signed,
Some(crypto_envelope),
);
Full Lifecycle
createPaymentIntent → enqueue → markProvisional → settle → finalize
↓ ↓
(if offline) (on-chain)
stays Pending SyncedFinalized
- Create — works offline, intent is queued locally
- Sync — when connectivity is available, the sync engine pushes to the network
- Settle — routed to Solana RPC via the settlement engine
- Finalize — confirmed on-chain, status becomes
SyncedFinalized
Fee Estimation
// Estimate the SOL fee before settlement
const estimatedFee = await estimateFee(intentId);
Fees cover Solana transaction costs. The caller must provide the RPC endpoint explicitly — there is no SDK default, forcing conscious choice of network (devnet, mainnet, custom).
Offline-Flow
Payment intents work fully offline during creation and queuing. When the device regains connectivity, the sync engine automatically pushes pending intents:
import { reportConnectivity } from '@zypp-labs/expo-sdk';
// Report connectivity changes
reportConnectivity('degraded');
reportConnectivity('good');
// The sync engine handles the rest automatically
Payment intents always route through the primary exit target (Solana L1) regardless of connectivity. They cannot be redirected through a degraded fallback.
Error Handling
| Error | Cause | Action |
|---|---|---|
DuplicateInLocalQueue | Same payment already queued | Check queue before retrying |
SignatureInvalid | Cryptographic signature failed | Re-create intent with valid signature |
L1Reversion | Solana transaction reverted | Retry with higher priority fee |
InsufficientBalance | Not enough funds | Fund wallet and retry |
Next Steps
- Learn how Tickets work for event access
- Dive into Settlement configuration
- See the Payments App example for a full reference implementation