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: { key: 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 { key } — 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
keystringAn 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 0x1::fungible_asset::Metadata reads symbol 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
}
}

Gasless — the exact rail (the buyer pays zero APT)

Section titled “Gasless — the exact rail (the buyer pays zero APT)”

Beyond onchain-proof, Aptos supports the ratified x402 exact rail (opt-in), and it’s gasless for the buyer. Aptos has native fee-payer (sponsored) transactions (AIP-39): the buyer signs a 0x1::primary_fungible_store::transfer to payTo with the gas sponsor (feePayer) set, signing only the sender slot — spending zero APT. The sponsor — your own relayer, or a keyless facilitator — adds the fee-payer signature and submits, paying the sub-cent gas. The buyer holds only the FA (USDC / USD₮):

import { requirePayment } from '@piprail/sdk'
requirePayment({
chain: 'aptos', token: 'USDC', amount: '0.10', payTo: '0x…',
exact: { settle: 'self', relayer: { key: process.env.APTOS_RELAYER_KEY! } }, // pays the gas to receive
})

It’s one-shot — the buyer needs only the advertised feePayer (no gas-station round-trip). Like Algorand, the relayer may be payTo itself (the fee-payer signature is separate from the transfer — no isolation rule), so a single merchant account can self-settle. Any Fungible Asset is gasless equally (USDC and USD₮ alike — there’s no EVM-style EIP-3009 token requirement). Native APT isn’t exact-payable — it stays onchain-proof. Live-proven on Aptos mainnet. Full mechanism: Gasless payments → Aptos.

Because your relayer co-signs a buyer-built transaction as the gas sponsor, the gate caps the gas the sponsor will pay before signing (MAX_GAS_AMOUNT_CAP = 100 000 units, MAX_GAS_UNIT_PRICE_CAP = 2 000 octas/unit — ≤ 2.0 APT worst case, i.e. 100 000 × 2 000 = 200 000 000 octas) — so a buyer can’t inflate max_gas_amount × gas_unit_price to drain it. See sponsor protection.

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