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). Use Pubkey::default() for native SOL
  • recipient: 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
  1. Create — works offline, intent is queued locally
  2. Sync — when connectivity is available, the sync engine pushes to the network
  3. Settle — routed to Solana RPC via the settlement engine
  4. 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

ErrorCauseAction
DuplicateInLocalQueueSame payment already queuedCheck queue before retrying
SignatureInvalidCryptographic signature failedRe-create intent with valid signature
L1ReversionSolana transaction revertedRetry with higher priority fee
InsufficientBalanceNot enough fundsFund wallet and retry

Next Steps