Skip to content

Aptos

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.

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:

Terminal window
npm install @aptos-labs/ts-sdk

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₮.

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 unlocked
const report = await res.json()

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:

FieldTypeNotes
privateKeystringAn AIP-80 ed25519-priv-0x… secret, or a raw 0x… hex key.
accountAccountA 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.

tokenWhat 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…',
})

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.

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.

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',
})