Aptos
Introduction
Section titled “Introduction”Aptos is a Move L1 with sub-second finality. PipRail treats it like any other family: name the
chain, name a token, add a wallet. The same requirePayment / PipRailClient calls you use on
EVM work here — only the wallet shape and the token model differ.
Aptos is the only Move L1 with both Circle-native USDC and Tether-native USD₮ built
in. Modern Aptos assets are Fungible Assets (FA), identified by a metadata object address
(a 0x… address), not a legacy Coin<T> type.
Install the peer dependency
Section titled “Install the peer dependency”The Aptos driver depends on @aptos-labs/ts-sdk, an optional peer. It is lazy-loaded on first
use, so a pure-EVM install never downloads it — but if you name chain: 'aptos' you must have it
installed:
npm install @aptos-labs/ts-sdkAccept a payment
Section titled “Accept a payment”Charge in USDC, USD₮, or native APT — the SDK fills in the FA metadata address and decimals from the built-in preset, so you never paste a token address:
import { requirePayment } from '@piprail/sdk'
app.get( '/report', requirePayment({ chain: 'aptos', token: 'USDC', amount: '0.10', payTo: '0x…' }), (req, res) => res.json({ report: 'unlocked' }),)payTo is an Aptos 0x… (32-byte) address. token: 'native' pays in APT (8 decimals — octas).
Native APT transfers move APT’s paired FA, so they emit the same
0x1::fungible_asset::Deposit event the stablecoins do — one pay/verify path covers native +
USDC + USD₮.
Pay a 402
Section titled “Pay a 402”On the buyer side, build a PipRailClient with an Aptos
wallet and fetch the URL — the client answers the 402 challenge and pays automatically:
import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'aptos', wallet: { privateKey: process.env.AGENT_KEY! }, // ed25519-priv-0x… or a raw 0x… 32-byte hex key})
const url = 'https://api.example.com/report'const res = await client.fetch(url)// → a normal Response — paid, settled, and unlockedconst report = await res.json()Wallet shape
Section titled “Wallet shape”An Aptos wallet is { privateKey } — an AIP-80 ed25519-priv-0x… secret, or a raw 0x…
32-byte hex key. If you built one yourself, pass a ready { account } (an
@aptos-labs/ts-sdk Account) instead:
| Field | Type | Notes |
|---|---|---|
privateKey | string | An AIP-80 ed25519-priv-0x… secret, or a raw 0x… hex key. |
account | Account | A ready @aptos-labs/ts-sdk Account, if you built it yourself. |
import { Account, Ed25519PrivateKey } from '@aptos-labs/ts-sdk'
const wallet = { account: Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(process.env.AGENT_KEY!), }),}See wallets by family for every family’s wallet shape.
Tokens
Section titled “Tokens”token | What it is |
|---|---|
'USDC' | Circle-native USDC — FA metadata + 6 decimals verified on-chain before shipping. |
'USDT' | Tether-native USD₮ — FA metadata + 6 decimals (on-chain symbol() reads USDt). |
'native' | APT, 8 decimals (octas). |
{ metadata, decimals } | Any other Fungible Asset, by its metadata object address. No allowlist. |
For a custom FA, pass the metadata object address — not the issuer/creator address:
requirePayment({ chain: 'aptos', token: { metadata: '0x…', decimals: 6 }, amount: '0.10', payTo: '0x…',})Receiver setup — none
Section titled “Receiver setup — none”Aptos needs no one-time receiver setup. Any valid Aptos address can receive: the recipient’s
primary FA store auto-creates on first deposit. The sender just needs APT for gas. You will
not see RecipientNotReadyError on Aptos.
Planning a payment before you spend
Section titled “Planning a payment before you spend”planPayment(url) reads balances, gas, and recipient readiness
on-chain and tells you whether a rail is settleable — without paying and without throwing. On
Aptos “I hold USDC but no APT for gas” surfaces as an INSUFFICIENT_GAS blocker rather than a
failed broadcast. It returns PaymentPlan | null (null when the URL isn’t payment-gated), so
null-guard the result first:
const url = 'https://api.example.com/report'const plan = await client.planPayment(url)
if (!plan) { await client.fetch(url) // not payment-gated — fetch it for free} else if (plan.payable) { await client.fetch(url) // safe — we checked} else { console.log(plan.fundingHint) // one human-readable line: what to top up (APT gas, or the token)}See planPayment() for the full PaymentPlan shape.
When a payment can’t go through
Section titled “When a payment can’t go through”Affordability always converges on one typed
InsufficientFundsError (.code === 'INSUFFICIENT_FUNDS') — whether
you’re short on the token or short on APT for gas. On Aptos the gas-token shortfall is the
headline trap: you hold USDC but no APT to send it. Catch it and read the .code:
import { PipRailError } from '@piprail/sdk'
const url = 'https://api.example.com/report'try { const res = await client.fetch(url) const report = await res.json()} catch (err) { if (err instanceof PipRailError && err.code === 'INSUFFICIENT_FUNDS') { // out of USDC, or out of APT for gas — fund the payer and retry console.error('Payer is short:', err.message) } else { throw err }}Proof binding — Template B (digest-bound)
Section titled “Proof binding — Template B (digest-bound)”Aptos uses Template B, like Sui, EVM, and Solana: the proof ref is the transaction hash.
verify() reads the committed tx, confirms it
succeeded and is within the recency window, then matches FA Deposit events against the
recipient’s primary store.
The binding never trusts the client. verify() re-derives payTo’s primary store for the
required metadata from the trusted accept, never the client-supplied ref — so a forged echo
can’t redirect it. Because a primary store address is metadata-specific, matching the store also
confirms the asset.
For the full picture of how the two templates differ, see Proof binding.
The built-in default fullnode (https://fullnode.mainnet.aptoslabs.com/v1) is rate-limited. In
production, pass your own rpcUrl — there’s no separate API-key field, so fold any key into the
URL:
requirePayment({ chain: 'aptos', token: 'USDC', amount: '0.10', payTo: '0x…', rpcUrl: 'https://your-aptos-fullnode.example.com/v1',})