Skip to content

Algorand

Algorand is a pure-PoS, payments-focused L1: ~3s single-step finality with no reorgs, and sub-cent fees. Name chain: 'algorand' and the driver auto-mounts on first use — a pure-EVM install never downloads its library.

Assets are ASAs (Algorand Standard Assets), identified by a numeric asset id rather than a contract address; native ALGO and ASAs both transfer in integer base units. Every transaction carries an arbitrary note field (up to 1 KB), which is where the challenge nonce rides.

The Algorand library is an optional peer, lazy-loaded the first time you name chain: 'algorand':

Terminal window
npm install algosdk

An Algorand wallet is { mnemonic } — a 25-word Algorand recovery phrase — or a ready { account } (an algosdk { addr, sk }, if you built the signer yourself):

import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({
wallet: { mnemonic: process.env.ALGORAND_MNEMONIC! }, // 25 words
chain: 'algorand',
})

payTo is a 58-character Algorand address. See Wallets by family for every family’s shape, and PipRailClient for the full client surface.

Name a token by symbol, 'native', or a custom ASA:

tokenWhat it is
'native'Native ALGO (6 decimals, microAlgos). Zero-setup — no opt-in needed.
'USDC'Circle’s native USDC (ASA 31566704, 6dp) — verified live on mainnet before shipping.
{ assetId, decimals }Any other ASA, by numeric asset id.
import { requirePayment } from '@piprail/sdk'
const accept = requirePayment({
chain: 'algorand',
token: 'USDC',
amount: '0.10',
payTo: 'YourAlgoAddr', // 58-char Algorand address
})
// → an X402Accept describing the rail (scheme, network, amount in base units, asset, payTo)

token: 'native' pays in ALGO and needs no opt-in at either end. It’s the simplest way to charge on Algorand: name the chain, name the amount, get paid.

import { requirePayment } from '@piprail/sdk'
const accept = requirePayment({
chain: 'algorand',
token: 'native',
amount: '0.10',
payTo: 'YourAlgoAddr',
})

ALGO is the volatile gas coin, so for stable pricing pay in USDC; for no-setup flows, native ALGO is ideal.

Receiving USDC needs a one-time ASA opt-in

Section titled “Receiving USDC needs a one-time ASA opt-in”

Before an account can receive USDC — or any ASA — it must opt into that asset once: a 0-amount asset-transfer to itself, which raises its minimum balance by 0.1 ALGO (locked, and recoverable). Without it, the payment fails. The payer is implicitly opted-in if it already holds USDC.

planPayment() surfaces a recipient that hasn’t opted in as a RECIPIENT_NOT_READY blocker before you spend, and a payment to an unready recipient raises a RecipientNotReadyError:

const plan = await client.planPayment('https://api.example.com/report')
if (!plan) {
// not payment-gated — nothing to plan
} else if (!plan.payable) {
console.log(plan.fundingHint) // → "must opt into the USDC ASA" etc.
}

The opt-in is plain algosdk — PipRail stays a payments SDK, not a wallet manager. Run it once per account, per ASA:

import algosdk from 'algosdk'
const account = algosdk.mnemonicToSecretKey(process.env.ALGORAND_MNEMONIC!) // { addr, sk }
const algod = new algosdk.Algodv2('', 'https://mainnet-api.algonode.cloud', '')
const sp = await algod.getTransactionParams().do()
const optIn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
sender: account.addr,
receiver: account.addr,
amount: 0,
assetIndex: 31566704,
suggestedParams: sp,
})
await algod.sendRawTransaction(optIn.signTxn(account.sk)).do() // one-time, per account per ASA

The payer needs a little ALGO for gas either way — fees are a flat 0.001 ALGO.

Algorand uses Template A: the challenge nonce rides in the transaction’s note field, so the proof is cryptographically bound to its challenge. verify() reads the merchant account’s inbound transfers via the indexer and re-derives every checked field from the trusted accept, never the client-supplied ref. See Verifying payments and Replay protection.

rpcUrl overrides the algod endpoint (used to submit transactions and fetch suggested params). The verify side reads inbound transfers from an indexer, which uses the public AlgoNode default — the public indexer is production-grade for this read, so overriding it is rarely necessary. The public algod is rate-limited; pass your own rpcUrl in production.