Download the portable boundary
JavaScript client · Client TypeScript declarations · Payment helpers · Payment TypeScript declarations · Third-party notices
Keep the four module/type files together. The client declarations reference the payment module. The payment helper is self-contained and includes the bundled dependency notices supplied alongside it. These downloads do not install any Sidecar or include your provider credentials.
Client behaviour
createSolanaClient(resolve) resolves the owning API at call time. available() reports methods currently exposed. call(component, method, input) allows only declared methods, requires a JSON object and bounds serialized arguments to 131,072 characters. Missing components reject explicitly. It does not start a provider, approve a wallet request or retry submission.
import {createSolanaClient} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);
console.log(client.available());
const status = await client.call('conduit', 'status');Install the modules as a matched set
Keep solana-client.mjs, solana-client.d.mts, solana-payments.mjs and solana-payments.d.mts together with their dependency notices. The declarations resolve the neighbouring .mjs modules; renaming one file without its matching declaration can break TypeScript resolution. These standalone modules are not the @capp/app-sdk package and the thin client does not embed the selected Sidecar runtimes.
In an app, pass a resolver for its actual trusted owner. On a server, the network-free payment module can be used without a window or any Sidecar. Do not make the server resolve a client's posted “runtime” object.
Presence, calls and failures
available() returns a per-component list of methods currently present, including empty lists for missing components. The resolver runs again for each call, so it can observe a late or removed provider. Presence does not prove a compatible network, a usable account or permission to sign.
Arguments are JSON-copied before invocation. Cycles and BigInt cannot be serialized; object properties containing functions or undefined are not portable data. Use decimal strings for atomic amounts, ordinary JSON objects for options and explicit declared method names. The 131,072 limit is serialized characters, not a transport-byte limit or a provider quota.
The client neither normalizes provider results nor checks them against schemas. Preserve a component's rejected promise/error and validate the fields your app needs. Never convert a failed balance read into zero or a timed-out submission into a new operation. The same method name on an untrusted object is not a trusted provider.
Exact amounts and a saved request
The following module creates request terms only. It never connects a wallet, contacts RPC, approves or sends. Your app must persist the returned request once and reuse that record; regenerating a reference or expiry changes the payment identity.
import { parseAmount, formatAmount, paymentReference, paymentRequest } from './solana-payments.mjs';
export async function createDevnetRequest({ requestId, recipient, amount, now }) {
if (typeof requestId !== 'string' || !requestId.trim()) throw new Error('A stable request ID is required');
if (typeof amount !== 'string') throw new Error('Enter the amount as decimal text');
if (!Number.isSafeInteger(now) || now <= 0 || now > 10000000000) throw new Error('Use Unix seconds, not milliseconds');
const amountAtomic = parseAmount(amount, 9);
const request = await paymentRequest({
requestId, network: 'devnet', recipient, asset: { kind: 'native' },
amountAtomic, reference: paymentReference(), createdAt: now, expiresAt: now + 600,
label: 'Example request', message: 'Review before paying',
});
return { request, displayAmount: formatAmount(request.amountAtomic, request.asset.decimals) };
}Use Math.floor(Date.now() / 1000) for now. This example also rejects values above 10,000,000,000 seconds to catch present-day millisecond timestamps; that bound belongs to the helper, not the protocol schema. parseAmount('0.01', 9) yields '10000000'; formatAmount('10000000', 9) yields '0.01'. Do not multiply a JavaScript floating-point number by a power of ten and assume exact money. paymentRequest() validates positive amount and request constraints; a valid request still does not establish payer approval, stored invoice ownership or a received payment.
tokenAddress() derives an address for the supported original SPL token layout; it does not create/fund the account. tokenAccount() decodes the supported raw account format, rejecting missing/frozen/uninitialized/wrapped-SOL or unsupported layouts. Neither helper authenticates a fetched account's provenance. The reviewed Edge execution path and token-program limits remain in Edge; broader descriptor types do not expand that execution policy.
Verification without signing authority
verifyPaymentTransaction() takes your server-owned saved request and your independently trusted RPC observations. It performs no network request itself. Use unmodified transaction bytes and metadata, the observed network genesis, commitment/status, slot and block time. Do not pass a client's invented balances, status or request terms as trusted evidence.
This helper makes the ownership of the two inputs explicit and ensures the observation cannot override the saved terms or required commitment:
import { verifyPaymentTransaction } from './solana-payments.mjs';
export async function verifyObservedPayment(savedRequest, trustedObservation) {
return await verifyPaymentTransaction({
...trustedObservation,
paymentRequest: savedRequest,
commitment: 'finalized',
});
}The required observation fields are in PaymentTransactionInput below. The output has paid: true, but also authorises: false and fulfilmentRecorded: false. A merchant still needs an atomic durable claim that binds payment/request to one intended order. Verification is not delivery, a replay store or cross-device deduplication.
hasBoundDevnetReceipt() is only a consistency check over an already-trusted Edge/Witness result. Arbitrary JSON can satisfy a consistency predicate; never use it as the independent verifier for a receipt posted by a customer.
Integration acceptance
Start with offline amount/request tests, then inspect missing-provider behaviour through the thin client. Use the reviewed devnet flow to separate prepare, preflight, approval, submit and observation. Preserve operation/attempt identities on a lost reply and use the component's recovery method rather than an application retry loop.
For merchant verification, test missing transactions, changed saved terms, incorrect network/commitment, insufficient recipient credit and duplicate fulfilment claims at your own service boundary. Source fixtures and valid cryptographic bytes are not a funded devnet or native-wallet qualification.
Client types
/** Thin application client. Provider-specific schemas remain authoritative. */
import type {PaymentRequest, PaymentRequestInput, PaymentVerification, SolanaAsset, SolanaNetwork} from './solana-payments.mjs';
export type JsonValue = null | boolean | number | string | JsonValue[] | {[key: string]: JsonValue};
export type JsonObject = {[key: string]: JsonValue};
export interface SolanaMethods {
edge: 'status'|'composition'|'prepare'|'preflight'|'authorise'|'submit'|'transactionStatus'|'receipt'|'prepareTokenTransfer'|'preparePayment'|'refresh';
compass: 'status'|'classify'|'intent'|'paymentRequest';
circuit: 'status'|'reserve'|'read'|'beginAttempt'|'transition'|'trace';
switchyard: 'status'|'allocate'|'read'|'transition'|'manifest';
accord: 'status'|'createGroup'|'group'|'request'|'approve'|'quorum';
conduit: 'status'|'request'|'verifyNetwork'|'inspect'|'openSettings'|'balance'|'asset'|'tokenAccounts'|'activity'|'transaction';
sentinel: 'status'|'evaluate'|'preflight';
latch: 'status'|'authorise'|'connect'|'walletList'|'walletCreate'|'walletImport'|'walletSelect'|'walletRename'|'walletRemove'|'walletUnlock'|'walletLock'|'walletExport'|'walletConnectExternal'|'walletDisconnectExternal'|'walletRecoveryStatus'|'walletRecover';
arena: 'status'|'execute'|'read'|'position'|'evidenceTransfer';
dispatch: 'status'|'submit'|'read'|'reconcile';
witness: 'status'|'observe'|'appendEvidence'|'receipt'|'readReceipt'|'trace'|'transactionEvidence'|'verifyPayment';
}
export type SolanaAPI = {[C in keyof SolanaMethods]?: Partial<Record<SolanaMethods[C], (input: JsonObject) => JsonValue | Promise<JsonValue>>>};
export declare const SOLANA_METHODS: Readonly<{[C in keyof SolanaMethods]: readonly SolanaMethods[C][]}>;
export interface SolanaClient {
available(): {[C in keyof SolanaMethods]: SolanaMethods[C][]};
call(component: 'latch', method: 'status', input?: JsonObject): Promise<JsonObject & {signingPolicyStatus?: boolean; signingPolicy?: SigningPolicyStatus}>;
call(component: 'compass', method: 'paymentRequest', input: PaymentRequestInput): Promise<PaymentRequest>;
/** Explicitly replace only an expired, never-submitted review. Requires fresh preflight and approval. */
call(component: 'edge', method: 'refresh', input: {operationId: string}): Promise<JsonObject>;
call(component: 'latch', method: 'walletCreate', input: {network: SolanaNetwork; label?: string; select?: boolean; password?: string; acknowledgeMainnetLoss?: boolean}): Promise<JsonObject>;
call(component: 'edge', method: 'prepareTokenTransfer', input: TokenTransferInput): Promise<JsonObject>;
call(component: 'edge', method: 'preparePayment', input: {operationId: string; walletPubkey: string; paymentRequest: PaymentRequestInput; legKey?: string}): Promise<JsonObject>;
call(component: 'witness', method: 'verifyPayment', input: {paymentRequest: PaymentRequestInput; signature: string; commitment?: 'confirmed' | 'finalized'}): Promise<PaymentVerification>;
call(component: 'conduit', method: 'asset', input: {network: SolanaNetwork; mint: string}): Promise<AssetObservation>;
call<C extends keyof SolanaMethods>(component: C, method: SolanaMethods[NoInfer<C>], input?: JsonObject): Promise<JsonValue>;
}
export declare function createSolanaClient(resolve?: () => SolanaAPI | undefined): SolanaClient;
/** Public configured limits only; absent on older Latch builds. */
export type SigningPolicyStatus = {
protocol: 'capp.solana-signing-policy.v1'; network: string;
enforcedLocally: boolean; maxLamports: number | null; tokenLimits: string | null;
authorises: false;
};
/** Binding check on trusted Edge/Witness results, not a verifier for imported receipts. */
export declare function hasBoundDevnetReceipt(operation: unknown): boolean;
export type {SolanaNetwork, AtomicAmount, AssetInput, SolanaAsset, PaymentRequest, PaymentRequestInput, PaymentVerification} from './solana-payments.mjs';
export interface TokenTransferInput {
operationId: string; network: 'devnet'; walletPubkey: string; recipient: string;
asset: {kind: 'spl-token'; mint: string; tokenProgram: string; decimals: number};
amountAtomic: string; legKey?: string; references?: string[];
commitment?: {namespace: string; sha256: string};
}
export interface AssetObservation {
protocol: 'capp.solana-asset-observation.v1'; asset: SolanaAsset; decimals: number; supplyAtomic: string;
mintAuthority: string | null; freezeAuthority: string | null; slot: number; genesisHash: string;
observedAt: number; source: 'rpc-observation'; metadataAuthenticated: false;
}
Payment helper types
The helper consumes trusted observations; it does not acquire or authenticate RPC evidence itself. amountAtomic values are strings. hasBoundDevnetReceipt() only checks already-trusted runtime results; use the independent verifier at a merchant trust boundary.
/** Network-free payment helpers. Feed verification with your own trusted RPC
* reads and server-owned request terms. No signing or fulfilment authority. */
export type SolanaNetwork = 'devnet' | 'mainnet-beta';
export type AtomicAmount = string;
export type AssetInput = {kind: 'native'; decimals?: 9} | {kind: 'spl-token'; mint: string; tokenProgram: string; decimals: number};
export type SolanaAsset = AssetInput & {protocol: 'capp.solana-asset.v1'; network: SolanaNetwork; genesisHash: string; decimals: number};
export interface PaymentRequestInput {
requestId: string; network: SolanaNetwork; recipient: string; asset: AssetInput;
amountAtomic: AtomicAmount; reference: string; createdAt: number; expiresAt: number;
label?: string; message?: string; memo?: string; requestDigest?: string;
}
export interface PaymentRequest extends PaymentRequestInput {
protocol: 'capp.solana-payment-request.v1'; genesisHash: string; asset: SolanaAsset;
requestDigest: string; uri: string; label: string; message: string; memo: string;
}
export interface PaymentVerification {
protocol: 'capp.solana-payment-verification.v1'; requestId: string; requestDigest: string;
network: SolanaNetwork; genesisHash: string; signature: string; slot: number; blockTime: number;
commitment: 'confirmed' | 'finalized'; confirmationStatus: 'confirmed' | 'finalized';
recipient: string; sender: string; asset: SolanaAsset; amountAtomic: string; receivedAtomic: string;
reference: string; transactionDigest: string; paid: true; simulated: false;
source: 'rpc-observation'; authorises: false; fulfilmentRecorded: false; evidenceDigest: string;
}
export interface PaymentTransactionInput {
paymentRequest: PaymentRequestInput; signature: string; transaction: [string, 'base64'];
/** Unmodified getTransaction meta from the trusted RPC. */
meta: Record<string, unknown>; slot: number; blockTime: number | null;
confirmationStatus: 'processed' | 'confirmed' | 'finalized'; genesisHash: string;
commitment?: 'confirmed' | 'finalized';
}
export declare const TOKEN: string;
export declare const GENESIS: Readonly<Record<SolanaNetwork, string>>;
export declare function assetDescriptor(network: SolanaNetwork, input?: AssetInput): SolanaAsset;
export declare function atomic(value: string, options?: {positive?: boolean}): AtomicAmount;
export declare function parseAmount(value: string, decimals: number): AtomicAmount;
export declare function formatAmount(value: AtomicAmount, decimals: number): string;
/** Derive the original SPL associated token address; no RPC or account creation. */
export declare function tokenAddress(owner: string, mint: string): Promise<string>;
/** Decode a trusted RPC base64 original SPL token account. Rejects missing,
* frozen, uninitialized, wrapped-SOL and unsupported account layouts. */
export declare function tokenAccount(value: {owner: string; executable: boolean; data: [string, 'base64']}): {mint: string; owner: string; amountAtomic: AtomicAmount};
export declare function paymentReference(): string;
export declare function paymentRequest(input: PaymentRequestInput): Promise<PaymentRequest>;
export declare function verifyPaymentTransaction(input: PaymentTransactionInput): Promise<PaymentVerification>;
Examples and protocol shapes
Reviewed payment · Independent verification · Schema reference