npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

periplo

v0.1.0

Published

Client library for x402 payments on Stellar, focused on upto: sign a spending ceiling once, settle only the real usage.

Readme

periplo

Client library for x402 payments on Stellar. Pay only for what was actually used, on Stellar testnet, with a facilitator handling verification and settlement.

Why upto

Most x402 payments are exact: the buyer signs a fixed amount, the seller charges exactly that. That works for a flat-rate API call, but not for anything priced by real, variable usage: tokens generated by an LLM call, seconds of access to a stream, rows returned by a query. Charging a flat rate for those either overcharges light users or undercharges heavy ones.

upto fixes this: the buyer signs a spending ceiling once, before usage is known. The seller measures what was actually used, and the facilitator settles only that amount against the ceiling, on-chain, in the same settlement transaction that would have moved the full ceiling. The buyer never has to trust the seller's meter: the Soroban contract that settles the payment enforces the ceiling itself and never holds funds between settlements.

Example: an API that generates tokens at 0.005 USDC/token. A buyer signs a ceiling of 0.1 USDC (up to 20 tokens). The service generates 7 tokens and stops. The facilitator settles 0.035 USDC, the buyer is never charged for the other 13 tokens' worth of ceiling, and the contract's balance is zero both before and after.

Install

npm install periplo

Quickstart: buyer

import { createUptoStellarPayer, payAndFetch, selectUptoStellarRequirement } from "periplo";

const payer = createUptoStellarPayer(process.env.BUYER_SECRET_KEY!, "stellar:testnet", {
  facilitatorBaseUrl: FACILITATOR_BASE_URL, // see note below the quickstarts
  // Hard local cap: reject any signed amount above this, before signing anything.
  maxSpendPerAuthorization: 1_000_000n, // 0.1 USDC/PTEST at 7 decimals
});

const result = await payAndFetch("https://example.com/paid-resource", payer, {
  // Required for upto: payAndFetch defaults to the `exact` scheme.
  selectRequirement: selectUptoStellarRequirement,
});

console.log(result.body); // the resource's response
console.log(result.settlement); // { transaction, amount, ... } once settled

payAndFetch does the whole 402 -> sign ceiling -> retry loop: it requests the resource, reads its 402 challenge, has payer sign the ceiling, retries with the signed payload attached, and returns the settled result.

Quickstart: seller

import { buildUptoRequirements, settleUptoUsage } from "periplo";

// One ceiling requirement, advertised in every 402 challenge for this route.
const ceiling = buildUptoRequirements({
  network: "stellar:testnet",
  payTo: SELLER_STELLAR_ADDRESS,
  asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", // testnet USDC
  settlementContract: "CA7OYVXWPSQHXNBWJQZ7TCKQILAVBHWDAPNRJTKY66LHR5K5LFXRV6TW",
  maxAmount: 1_000_000n, // 0.1 USDC ceiling
});

// Respond 402 with { accepts: [ceiling] } when unpaid, per the x402 wire
// protocol (@x402/core/http's own header helpers).

// Once the buyer's signed payload arrives, do the metered work, then
// settle for what was actually used (must be <= the signed ceiling):
const tokensGenerated = 7;
const pricePerToken = 50_000n; // 0.005 USDC
const actualAmount = pricePerToken * BigInt(tokensGenerated);

const settlement = await settleUptoUsage(paymentPayload, ceiling, actualAmount, {
  facilitatorBaseUrl: FACILITATOR_BASE_URL, // see note below the quickstarts
});

console.log(settlement.transaction); // the real Stellar testnet transaction hash

FACILITATOR_BASE_URL must point at a facilitator with upto configured for stellar:testnet (see Status and limitations: the public https://periplo-testnet.fly.dev does not have it configured today).

Both quickstarts above are exercised for real against stellar:testnet as part of this package's own release process (see Evidence below): they are not illustrative pseudocode.

How it works

  • The buyer signs a Soroban authorization tying together the asset, the recipient, and the ceiling (max_amount). The signature covers that tuple only, never the eventual real amount, so it stays valid even though the real amount isn't known yet.
  • The facilitator settles the real amount, which must be less than or equal to the signed ceiling. The UptoSettlement contract enforces this on-chain: it can never move more than the buyer authorized.
  • Each authorization carries a single-use nonce, so a signed ceiling can settle exactly once.
  • The contract never holds funds between settlements: it pulls the ceiling, pays the seller the real amount, and refunds the difference to the buyer, all inside the one settlement transaction. Its balance is zero before and after every call, which you can confirm yourself against Horizon.

Networks and assets

Only stellar:testnet is supported today. Any other network, including Stellar pubnet/mainnet, throws UptoMainnetNotSupportedError explicitly: this project has no mainnet fee-sponsor key, so nothing about upto mainnet support is implied or half-built.

  • UptoSettlement contract: CA7OYVXWPSQHXNBWJQZ7TCKQILAVBHWDAPNRJTKY66LHR5K5LFXRV6TW
  • Testnet USDC (SAC): CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
  • PTEST, a self-issued testnet asset with no value (SAC): CCK2UCUDA2CYGBHIPURM6TIXZEHULBVIGPVB2UTP3R2LCIKB3O5P723X

API reference

Buyer, exact (existing scheme)

  • createExactStellarPayer(secretKey: string, network: Network): PaymentPayer Signs and pays a fixed exact amount.

Buyer, upto

  • createUptoStellarPayer(secretKey: string, network: Network, options: CreateUptoStellarPayerOptions): PaymentPayer Signs an upto spending ceiling. options:
    • facilitatorBaseUrl: string
    • maxSpendPerAuthorization: bigint
    • rpcUrl?: string
    • deadlineLedgerOffset?: number (ledgers the signed ceiling stays valid for; default 30)
    • facilitatorClient?: Pick<HTTPFacilitatorClient, "getSupported"> (for tests)
  • selectUptoStellarRequirement(accepts: readonly PaymentRequirements[], network: string): PaymentRequirements | undefined Pure selector: the first upto/contract-profile option for network.

Buyer, scheme-agnostic

  • payAndFetch(resourceUrl: string, payer: PaymentPayer, options?: PayAndFetchOptions): Promise<BuyerFetchResult> options.selectRequirement defaults to selectExactStellarRequirement; pass selectUptoStellarRequirement to pay an upto challenge.
  • discoverPayAndFetch(facilitatorBaseUrl: string, query: string, payer: PaymentPayer, options?: PayAndFetchOptions): Promise<BuyerFetchResult & { resource: DiscoveryResource }> Same selectRequirement option, applied to Bazaar discovery.
  • searchBazaar, selectPayableResource, resolveResourceRequestUrl, selectExactStellarRequirement: lower-level pieces payAndFetch/discoverPayAndFetch are built from.
  • PaymentPayer (interface): { network: string; createPaymentPayload(paymentRequired): Promise<PaymentPayload> }. Implement this yourself to plug in a different signer.

Seller, upto

  • buildUptoRequirements(config: UptoRequirementConfig): PaymentRequirements Builds the ceiling requirement to advertise. config: network, payTo, asset, settlementContract, maxAmount: bigint, maxTimeoutSeconds?, resource?.
  • verifyUptoPayment(paymentPayload, ceilingRequirements, options: UptoFacilitatorOptions): Promise<VerifyResponse>
  • settleUptoUsage(paymentPayload, ceilingRequirements, actualAmount: bigint, options: UptoFacilitatorOptions): Promise<SettleResponse> Rejects locally (no facilitator call) if actualAmount is negative or exceeds the signed ceiling. UptoFacilitatorOptions: { facilitatorBaseUrl: string; facilitatorClient?: Pick<HTTPFacilitatorClient, "verify" | "settle"> }.

Seller, discovery metadata (scheme-agnostic)

  • definePaidResource(config: PaidResourceConfig): PaidResourceDeclaration Declares Bazaar discovery metadata and a matching query-parameter parser from one spec, so they can't drift apart.

Error codes

Every rejection this library throws carries a stable code (in addition to .name and a non-empty .message), so callers can branch on it without parsing message text.

| code | class | thrown when | | --- | --- | --- | | upto_mainnet_not_supported | UptoMainnetNotSupportedError | createUptoStellarPayer called for any network other than stellar:testnet | | upto_no_matching_requirement | NoUptoPaymentOptionError | the 402 challenge has no upto/contract option for this network, or it's missing extra.settlementContract | | upto_spending_ceiling_exceeded | SpendingCeilingExceededError | the offered ceiling exceeds the configured maxSpendPerAuthorization, checked before any signing | | upto_facilitator_signer_unavailable | UptoFacilitatorSignerUnavailableError | the facilitator's GET /supported lists no signer for this network | | upto_simulation_failed | UptoSimulationError | simulating the ceiling-signing transaction failed | | upto_settlement_exceeds_ceiling | UptoSettlementExceedsCeilingError | settleUptoUsage called with a real amount above the ceiling, or negative | | upto_settlement_failed | UptoSettlementFailedError | the facilitator rejected or failed the settlement; .errorReason carries its own reason | | no_acceptable_payment_option | NoAcceptablePaymentOptionError | the 402 challenge has no option matching selectRequirement | | no_discoverable_resource | NoDiscoverableResourceError | Bazaar search returned nothing payable | | payment_failed | PaymentFailedError | the paid retry itself was rejected; .status carries the HTTP status | | unexpected_response | UnexpectedResponseError | any other non-2xx/402 response; .status carries the HTTP status |

Security

  • Non-custodial by construction. The UptoSettlement contract's balance is zero before and after every settlement; it never holds funds between calls. Verify this yourself against Horizon for any settlement hash below.
  • The ceiling is checked before signing, not after. maxSpendPerAuthorization is enforced locally, in this library, before any Stellar RPC call or signature is produced.
  • Secrets never leave the process. Keypair.fromSecret holds the secret in memory; only signed output (a signature, a signed transaction) is ever returned.
  • Report a vulnerability: github.com/Eras256/Periplo/security.

Status and limitations

  • The upto wire format follows the draft spec proposed in x402-foundation/x402#3098. That PR is open, not merged, checked against the GitHub API at the time of each release; this library implements a draft, not an approved standard.
  • stellar:testnet only. No mainnet support, by design (see Networks and assets).
  • No external security audit has been performed on this library or on the UptoSettlement contract it settles through.
  • The publicly deployed facilitator (https://periplo-testnet.fly.dev) advertises exact only as of this release; it does not yet have upto configured, checked live against its own GET /supported at the time of writing. The quickstarts above work against any facilitator running apps/facilitator's own code with upto configured (see this repo's docs/TOOLING.md); the evidence transaction above was produced against such an instance, not the public URL.
  • Unit tests cover the deterministic logic (ceiling checks, requirement selection/construction, every error code) without needing network access. The signing and settlement path itself is proven by running the quickstarts above for real against testnet before each release (see Evidence), not by a mocked integration test.

Evidence

Real stellar:testnet settlement transactions, produced by running this package's own quickstarts from a packed tarball, outside the monorepo:

License

Apache-2.0