Skip to content

Wallets by family

A PipRailClient takes one wallet and one chain. The chain selector routes to a driver family, and each family validates its own key format — so the wallet shape you pass depends on the chain you name. Pass the wrong shape for the chain and you get a clear WrongFamilyError on first use, before any funds move.

import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'base', wallet: { privateKey: process.env.AGENT_KEY } })
// → a PipRailClient bound to Base, ready to quote / plan / pay

Each family accepts a primary secret-key form and (where it makes sense) a ready-built native wallet object. The selector picks the family; the wallet must match it.

FamilyWallet shape
EVM ('base', 'bnb', … any EVM chain){ privateKey } (0x… hex) or a viem { walletClient }
Tron{ privateKey } (32-byte hex — secp256k1, like EVM)
Sui{ privateKey } (suiprivkey1… bech32) or { keypair }
Aptos{ privateKey } (AIP-80 ed25519-priv-0x… or raw 0x… hex) or { account }
Solana{ secretKey } (Uint8Array or base58 string) or { signer }
TON{ mnemonic } (24 words) or { keyPair } (+ version: 'v5r1' for W5)
Algorand{ mnemonic } (25 words) or { account } (algosdk { addr, sk })
Stellar{ secret } (S… seed) or { keypair }
XRPL{ seed } (s… seed) or { wallet }
NEAR{ accountId, privateKey } (privateKey = ed25519:… secret)

The secp256k1 and Ed25519 families take a raw private key string. EVM and Tron both use secp256k1 (Tron’s key is the same 32-byte hex), while Sui and Aptos take their own encoded forms.

new PipRailClient({ chain: 'base', wallet: { privateKey: process.env.AGENT_KEY } }) // 0x… hex
new PipRailClient({ chain: 'tron', wallet: { privateKey: process.env.TRON_KEY } }) // 32-byte hex
new PipRailClient({ chain: 'sui', wallet: { privateKey: process.env.SUI_KEY } }) // suiprivkey1…
new PipRailClient({ chain: 'aptos', wallet: { privateKey: process.env.APTOS_KEY } }) // ed25519-priv-0x…

Sui also accepts a ready { keypair } (an Ed25519Keypair); Aptos accepts a ready { account }.

Solana takes a secretKey as either a Uint8Array or a base58 string, or a ready { signer }.

new PipRailClient({ chain: 'solana', wallet: { secretKey: process.env.SOLANA_SECRET } }) // base58

Both take a mnemonic — TON uses 24 words, Algorand uses 25. The mnemonic may be one space-separated string or a string[].

new PipRailClient({ chain: 'ton', wallet: { mnemonic: process.env.TON_MNEMONIC } }) // 24 words
new PipRailClient({ chain: 'algorand', wallet: { mnemonic: process.env.ALGO_MNEMONIC } }) // 25 words

On TON, add version: 'v5r1' for a W5 wallet — the default is v4. Algorand also accepts a ready { account } (an algosdk { addr, sk }).

Stellar takes a secret (an S… seed); XRPL takes a seed (an s… seed). Each also accepts its native wallet object.

new PipRailClient({ chain: 'stellar', wallet: { secret: process.env.STELLAR_SECRET } }) // S…
new PipRailClient({ chain: 'xrpl', wallet: { seed: process.env.XRPL_SEED } }) // s…

Stellar accepts a ready { keypair } (a stellar-sdk Keypair); XRPL accepts a ready { wallet } (an xrpl.js Wallet).

NEAR is the one family that needs two fields: the named account it signs as plus its key. The privateKey is an ed25519:… secret.

new PipRailClient({
chain: 'near',
wallet: { accountId: 'agent.near', privateKey: process.env.NEAR_KEY },
})

Injected browser wallet — { walletClient }

Section titled “Injected browser wallet — { walletClient }”

On EVM you can hand the client an injected viem walletClient instead of a raw key — the visitor signs with their own wallet, and no secret ever touches your page source.

import { createWalletClient, custom } from 'viem'
const walletClient = createWalletClient({ transport: custom(window.ethereum) })
const client = new PipRailClient({ chain: 'base', wallet: { walletClient } })

The chain picks the family, and the family validates the wallet on first use. If the shape (or a payTo, or a token) is given in another family’s form — an 0x… address on Solana, a { mint } token on a Stellar chain, a { seed } wallet on EVM — the bind throws a typed WrongFamilyError (.code === 'WRONG_FAMILY') rather than failing obscurely later.

import { PipRailClient, WrongFamilyError } from '@piprail/sdk'
// chain says EVM, wallet is an XRPL seed → WrongFamilyError on first request
const client = new PipRailClient({ chain: 'base', wallet: { seed: 's…' } })
try {
await client.fetch('https://api.example.com/report')
} catch (err) {
if (err instanceof WrongFamilyError) {
console.error(err.code, err.message) // → 'WRONG_FAMILY' '…an XRPL seed on an EVM chain…'
} else {
throw err
}
}

The error is raised lazily — the driver auto-mounts and binds the wallet on the first call (see the PaymentDriver architecture), so a misconfigured client constructs fine but throws the moment it tries to act.

Per-chain receive prerequisites, token coverage, and proof binding live on each chain’s page — start at the chains overview. Once a wallet is bound, the read-only planPayment() tells you whether it can actually settle a given 402.