Catalog npm
PACKAGE EXPLORERView in registry

@0gfoundation/0g-pay-sdk

npm

0G Pay SDK - TypeScript/React library for 0G payment integration

0g0glabspaypaymentreactsdktypescriptweb3
Versions collected20
Direct dependencies13From release 0.4.0
Last observed20 Oct, 22:11 UTC

README

Current package documentation

0G Pay SDK

SDK for accepting deposits and card payments on the 0G network.

  • Crypto / Card via TokenFlight — swap any token or pay by card through the TokenFlight widget
  • Stripe recharge — top up USD balance directly via Stripe hosted checkout, using the 0G Pay backend
  • StripeClient — headless Node.js / server-side client: checkout, balance, usage, topup history — no React required
  • /server subpath — React-free bundle for pure Node.js projects (no react/react-dom peer deps needed)

Installation

npm install @0gfoundation/0g-pay-sdk

Peer dependencies (React projects only)

npm install react react-dom

If you only need the Node.js / server-side client (StripeClient), import from the /server subpath — no React peer deps required:

import { StripeClient } from '@0gfoundation/0g-pay-sdk/server';

Quick Start

Crypto / Card deposit (TokenFlight)

import { OGPay } from '@0gfoundation/0g-pay-sdk';

export function App() {
  const provider = window.ethereum; // any EIP-1193 provider

  return <OGPay provider={provider} />;
}

Clicking the Deposit button opens a modal with the TokenFlight widget, letting users swap any token or pay by card.

Stripe recharge (credit card → USD balance)

import { OGPay } from '@0gfoundation/0g-pay-sdk';

export function App() {
  const provider = window.ethereum;

  return (
    <OGPay
      provider={provider}
      stripe={{
        network: 'testnet',
        sessionToken: yourSessionToken,
        successUrl: `${window.location.origin}/topup/return`,
      }}
    />
  );
}

When stripe is provided, the modal gains a Card (Stripe) tab. Users pick a USD amount, click pay, and are redirected to Stripe hosted checkout. After payment, Stripe redirects back to your successUrl.


Stripe Recharge — Step-by-Step Tutorial

The Stripe flow has three parts: obtaining a session token, triggering the checkout, and handling the return page.

Step 1 — Get a session token

The 0G Pay backend requires a session token for all API calls. You exchange your user's Privy access token for a Pay session token once per session.

import { getSessionUrl } from '@0gfoundation/0g-pay-sdk';

async function getPaySessionToken(privyAccessToken: string): Promise<string> {
  const res = await fetch(getSessionUrl('testnet'), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ privy_token: privyAccessToken }),
  });
  if (!res.ok) throw new Error(`Session exchange failed: ${res.status}`);
  const { token } = await res.json();
  return token;
}

Store the token in component state or your auth context — it is valid until the JWT expires.

Step 2 — Render the deposit button with Stripe config

Pass the session token (and optionally an email) into the stripe prop. The successUrl must point to a page in your app that renders the StripeReturn component (see Step 3).

import { OGPay } from '@0gfoundation/0g-pay-sdk';

export function TopupButton({ sessionToken, email }: { sessionToken: string; email?: string }) {
  const provider = window.ethereum;

  return (
    <OGPay
      provider={provider}
      stripe={{
        network: 'testnet',
        sessionToken,
        email,                                              // pre-fills Stripe checkout
        successUrl: `${window.location.origin}/topup/return`, // your return page
        cancelUrl: window.location.href,                    // where to go on cancel
      }}
    />
  );
}

When the user opens the modal and clicks the Card (Stripe) tab, they see:

  • Amount presets: $10 · $50 · $100 · $500
  • A custom amount input
  • A Pay $X with Card button

Clicking the button calls POST /v1/checkout on the Pay backend and redirects the browser to the Stripe hosted checkout page.

Step 3 — Handle the return page

After payment, Stripe appends ?session_id=cs_... to your successUrl and redirects the user there. Render the StripeReturn component on that page — it polls the backend until the payment is confirmed (up to 18 seconds) and shows the result.

// pages/topup/return.tsx (or your router equivalent)
import { StripeReturn } from '@0gfoundation/0g-pay-sdk';
import { useNavigate } from 'react-router-dom';

export function TopupReturnPage({ sessionToken }: { sessionToken: string }) {
  const navigate = useNavigate();
  const sessionId = new URLSearchParams(window.location.search).get('session_id') ?? '';

  if (!sessionId) return <p>Invalid return URL — missing session_id.</p>;

  return (
    <StripeReturn
      sessionId={sessionId}
      network="testnet"
      sessionToken={sessionToken}
      onConfirmed={(creditedUsd) => {
        console.log(`$${creditedUsd.toFixed(2)} added to balance`);
        navigate('/dashboard');
      }}
    />
  );
}

StripeReturn renders three states:

StateShown when
Spinner + "Confirming your payment…"Polling in progress
Green "Payment confirmed! $X.XX added"Backend confirms paid: true
Amber "Balance will update shortly"18-second timeout — payment likely succeeded but backend is slow

Step 4 — (Optional) standalone hooks

If you need tighter control, use the hooks directly instead of the pre-built components.

useStripeCheckout — create a session and redirect programmatically:

import { useStripeCheckout } from '@0gfoundation/0g-pay-sdk';

function CustomTopupForm({ sessionToken }: { sessionToken: string }) {
  const { createSession, loading, error } = useStripeCheckout({ network: 'testnet', sessionToken });

  return (
    <>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button disabled={loading} onClick={() => createSession(50)}>
        {loading ? 'Redirecting…' : 'Pay $50 with Card'}
      </button>
    </>
  );
}

useStripeReturn — poll payment status and render your own UI:

import { useStripeReturn } from '@0gfoundation/0g-pay-sdk';

function MyReturnPage({ sessionId, sessionToken }) {
  const { status, creditedMicros, availableMicrosAfter } = useStripeReturn(sessionId, {
    network: 'testnet',
    sessionToken,
  });

  if (status === 'loading') return <Spinner />;
  if (status === 'timeout') return <p>Payment received — balance updating soon.</p>;

  const creditedUsd = creditedMicros / 1_000_000;
  return <p>✓ ${creditedUsd.toFixed(2)} credited to your account.</p>;
}

Stripe Client (Node.js / server-side)

StripeClient is a headless client with no React or browser dependencies. Use it in Node.js servers, serverless functions, scripts, or any environment where you need to drive the 0G Pay API programmatically.

Requires Node.js 18+ (native fetch and AbortSignal.timeout).

Import

// In a React project (react already installed):
import { StripeClient } from '@0gfoundation/0g-pay-sdk';

// In a pure Node.js project (no React needed):
import { StripeClient } from '@0gfoundation/0g-pay-sdk/server';

Setup

const client = new StripeClient({
  network: 'testnet',       // 'testnet' | 'mainnet' — defaults to 'testnet'
  sessionToken: '...',      // session token from exchangeSession()
});

Use baseUrl to point at your own proxy or a host other than the two network defaults:

const client = new StripeClient({
  baseUrl: 'https://your-pay-backend.example.com', // overrides network
  sessionToken: '...',
});

Exchange session token

Exchange a Privy access token for a 0G Pay session token. The returned token is used as sessionToken for all other methods.

const { token, account_id } = await client.exchangeSession(privyAccessToken);

Get balance

const { available_micros } = await client.getBalance();
const usd = available_micros / 1_000_000;

Create a checkout session

const { session_id, url } = await client.createCheckoutSession({
  amountUsd: 50,                              // or use amountMicros: 50_000_000
  successUrl: 'https://yourapp.com/return',
  cancelUrl:  'https://yourapp.com/cancel',
  email: 'user@example.com',                 // optional — pre-fills Stripe checkout
});
// url is the Stripe hosted checkout page — redirect the user there

amountUsd and amountMicros are mutually exclusive; exactly one must be provided.

Poll payment status

import { StripeClient, StripeAuthError, StripePollingTimeoutError } from '@0gfoundation/0g-pay-sdk/server';

try {
  const result = await client.pollCheckoutStatus(session_id, {
    maxAttempts: 6,    // default 6
    intervalMs: 3_000, // default 3000 ms
  });
  if (result.paid) {
    console.log(`Credited $${result.credited_micros / 1_000_000}`);
  } else {
    // All attempts returned paid: false — payment not yet confirmed.
    // This is a normal result for reconcile calls (e.g. page load checks).
  }
} catch (err) {
  if (err instanceof StripeAuthError) {
    // 401 or 403 — session token is invalid or expired. Refresh and retry.
    console.error(`Auth failed (${err.status}): ${err.message}`);
  } else if (err instanceof StripePollingTimeoutError) {
    // Never received a clean response in maxAttempts tries (network/server error).
    // err.cause holds the underlying error.
    console.warn(`No response after ${err.attempts} attempts`, err.cause);
  } else {
    throw err;
  }
}

Polling behaviour:

  • Returns immediately when paid: true.
  • If all attempts return paid: false, resolves with the last status — not an error. This makes single-attempt reconcile calls (maxAttempts: 1) work naturally.
  • Throws StripeAuthError immediately on 401/403 — no retries.
  • Throws StripePollingTimeoutError (with .cause) only when no clean HTTP response was received at all.

List topup history

const { topups, next_before } = await client.listTopups({ limit: 20 });
// paginate: pass next_before as `before` on the next call
const page2 = await client.listTopups({ limit: 20, before: next_before });

Pass source to filter by payment origin. This is important when checking whether a topup is a user's first real deposit — the backend may return credit-type rows before any Stripe or on-chain topup, causing a naïve "first row" check to misfire:

// Only stripe and on-chain topups — correct for "is first topup" checks
const { topups } = await client.listTopups({ limit: 1, source: 'stripe,chain' });
const isFirstTopup = topups.length === 0;

Get usage

const { items, cursor } = await client.getUsage({ limit: 50 });
const page2 = await client.getUsage({ limit: 50, cursor });

Get credits

Lists the account's credit lots — promotional bonuses, staking rewards and team grants — in spend order (soonest expiry first).

A page at a time, so walk it to the end before deriving any money figure from it:

const LIMIT = 500; // the backend's ceiling

const MAX_PAGES = 20; // a bound, not an expectation

// `extra` picks WHICH lots — leave it off to walk every lot, pass
// { state: 'granted' } for only the spendable ones. It deliberately cannot
// carry `limit` or `cursor`: the walk owns those, and a caller who lowered
// `limit` would make the completeness test below read a full page as short.
async function walkCreditLots(
  extra: Pick<GetCreditsParams, 'state'> = {},
): Promise<{ lots: CreditLot[]; complete: boolean }> {
  const lots: CreditLot[] = [];
  let cursor: string | undefined;

  for (let i = 0; i < MAX_PAGES; i++) {
    const page = await client
      .getCredits({ ...extra, limit: LIMIT, cursor })
      .catch(() => null);
    // Keep what arrived and call it partial. A 429 here is routine — this
    // endpoint allows 20 requests a minute across every reader of it — and
    // throwing would discard pages already in hand.
    if (!page) return { lots, complete: false };

    lots.push(...page.lots);

    // The server's cursor is the only way on. Where none comes back, a short
    // page is the end of the list and a full one is a list this deployment
    // cannot let you finish.
    if (!page.next_cursor) return { lots, complete: page.lots.length < LIMIT };
    cursor = page.next_cursor;
  }
  return { lots, complete: false };
}

const { lots, complete } = await walkCreditLots();
if (!complete) {
  // Do not derive a money figure from this. Show what you have and say it is partial.
}
for (const lot of lots) {
  // `source` is the category to branch on, `display` is the label to render.
  console.log(lot.source, lot.display, lot.state, lot.remaining_micros);
}

Never build a cursor yourself. It is tempting — pay-v2's cursor is `${lot.expires_at}|${lot.lot_id}`, it calls the format "opaque by convention", and the cursor request parameter has always been forwarded. The reason not to is that you cannot tell which kind of deployment answered you, and one of them does not terminate.

Reading ?cursor and reporting next_cursor shipped in one pay-v2 commit, so a pay-v2 old enough to report no cursor also ignores the one you send — and hands back the same first page. A walk that synthesizes a cursor there appends that page forever. (The proxy in front can also report no cursor while passing ?cursor through to a backend that does honour it, in which case a built cursor would have worked. Nothing in the response distinguishes the two, so guessing buys a working walk on one host and an infinite loop on the other.)

That is what complete is for. Where no cursor comes back, treat the list as unfinishable and say so — a partial list that admits it beats a figure that is quietly wrong.

Show state, never derive liveness yourself — it is computed against the request's own clock, so an expired-but-unswept lot reports 'expired' rather than looking spendable:

// Credit this account can actually spend right now.
// `state: 'granted'` makes this one request on any real account; the filter stays
// regardless — see below.
const walked = await walkCreditLots({ state: 'granted' });
const spendable = walked.complete
  ? walked.lots
      .filter(lot => lot.state === 'granted' && lot.remaining_micros > 0)
      .reduce((sum, lot) => sum + lot.remaining_micros, 0)
  : null; // an incomplete list cannot produce this figure — say so, do not guess

state: 'granted' does not replace that filter. The backend's predicate for the parameter is status = 'active' AND expires_at > now, which — unlike the authoritative spendable sum — does not include remaining_micros > 0. A drained-but-unswept lot therefore still comes back as 'granted' with nothing left. Keeping the filter is also what makes the parameter safe to pass: a deployment that predates it ignores it silently, and with the filter in place that costs extra rows rather than a wrong figure.

Pass it only where you want spendable lots. It is a filter on the list, so any figure that needs the dead lots must be walked without it — available_micros, for instance, counts unswept expired credit, and a display that shows a user their clawed-back bonus needs the revoked lot to still arrive.

Testing whether a deployment honours it can only ever come back "no". Ask for 'granted': if a lot arrives in another state, the parameter was ignored. If every lot is 'granted', you have learnt nothing — an account whose lots happen to all be live looks identical either way. So never conclude "honoured, one request is enough" from a clean result; keep the walk.

Why the walk is not optional. Lots come back expires_at ASC, and an 'expired' lot's expiry is in the past by definition — so expired lots sort ahead of every live one and hold the page open. 'spent' lots tend to sort early too, having drained in that order. 'revoked' does not: a revocation is a refunded topup or a funder pulling credit back, and it leaves expires_at alone, so a lot granted today for twelve months and revoked an hour later sorts at the end. The dead lots are therefore not a prefix you may stop scanning at, and a dead lot in the last row says nothing about whether the list is complete.

Once an account holds more lots than limit, the rows that fall off the end are the live credit. A credit sum from that page under-reports credit, and the usual deposit split — spendable_micros − Σ(credit), needed because Pay does not expose funds lots — over-reports deposit by exactly the same amount. That shows granted credit as the user's own money. pay-v2 hit this on staging on 2026-09-22 (#23).

Once the walk is complete, that subtraction is exact and is the way to get a deposit figure. Balance.spendable_micros on its own cannot give you one — it is deposit plus credit, which is why the split exists.

Which hosts honour what is a deployment question, not an SDK one. At the time of writing the testnet host reports next_cursor and honours state; the mainnet host does neither. That will change without this README changing, so do not code against the sentence you just read — code against complete and against the state check above, and the walk stays correct on either.

Breaking change (backend, 2026-09-20). CreditLot.status is gone; use state. The old field was the expiry sweep's bookkeeping column — it read "active" for up to an hour after credit had expired, and it could not tell an expiry from a revocation. state answers both, correctly.

Full server-side flow

import { StripeClient, StripeAuthError, StripePollingTimeoutError } from '@0gfoundation/0g-pay-sdk/server';

const PAY_URL = 'https://your-pay-backend.example.com';

// 1. Exchange Privy token once per user session
async function getClient(privyToken: string) {
  const client = new StripeClient({ baseUrl: PAY_URL, sessionToken: '' });
  const { token, account_id } = await client.exchangeSession(privyToken);
  return new StripeClient({ baseUrl: PAY_URL, sessionToken: token, accountId: account_id });
}

// 2. Create a checkout session and return the Stripe URL to the frontend
async function createTopup(privyToken: string, amountUsd: number, returnUrl: string) {
  const client = await getClient(privyToken);
  const { session_id, url } = await client.createCheckoutSession({
    amountUsd,
    successUrl: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`,
    cancelUrl: returnUrl,
  });
  return { session_id, url };
}

// 3. After Stripe redirects back, confirm payment and read updated balance
async function confirmTopup(privyToken: string, sessionId: string) {
  const client = await getClient(privyToken);
  try {
    const status = await client.pollCheckoutStatus(sessionId);
    const balance = await client.getBalance();
    return { status, balance };
  } catch (err) {
    if (err instanceof StripeAuthError) throw new Error('Session expired');
    if (err instanceof StripePollingTimeoutError) return { timedOut: true };
    throw err;
  }
}

Partner API

The Partner API is a set of public, read-only HTTP endpoints exposed by the 0G Pay portal. No authentication is required — call them directly with plain fetch.

Use getApiUrl() to resolve the correct base URL for each network:

import { getApiUrl } from '@0gfoundation/0g-pay-sdk';

const BASE = getApiUrl('testnet'); // 'https://0g-pay-scan.vercel.app'

Campaigns

A campaign is a funded reward pool. Partners query campaigns to display reward availability to users.

List campaigns

GET /api/partner/campaigns
const res = await fetch(`${BASE}/api/partner/campaigns`);
const data = await res.json(); // { campaigns: Campaign[] }
Query paramTypeDescription
statusstringFilter by status: active | closed | expired | pending_approval | withdrawn
limitnumberMax records to return

Get campaign detail

GET /api/partner/campaigns/:id
const res = await fetch(`${BASE}/api/partner/campaigns/${campaignId}`);
const campaign = await res.json();

Returns the full campaign object including its associated lots (credit allocations).

Each lot reports state (granted | spent | expired | revoked), the same computed field as CreditLot.state. As of the 2026-09-20 backend release it no longer carries the raw status column, which lagged behind reality by up to an hour and showed expired credit as active.

Get campaign rollup stats

GET /api/partner/campaigns/:id/rollup
const res = await fetch(`${BASE}/api/partner/campaigns/${campaignId}/rollup`);
const stats = await res.json();

Returns aggregated stats for the campaign — total granted, total remaining, user counts, etc.


Programs

A program defines the reward rules that govern how credits are calculated and distributed within a campaign. Programs expose their schedule, credit validity period, and two-tier rate configuration.

List programs

GET /api/partner/programs
const res = await fetch(`${BASE}/api/partner/programs`);
const data = await res.json(); // { programs: Program[] }

A sample response — two built-in programs, topup-promo (which carries terms) and staking (which does not):

{
  "programs": [
    {
      "program": "topup-promo",
      "running": true,
      "starts_at": "2026-09-11T00:00:00Z",
      "expires_at": "2026-09-26T23:59:59Z",
      "credit_duration_secs": 300000000,
      "live": true,
      "terms": {
        "first_topup": { "bonus_pct": 100, "cap_micros": 10000000000,
                         "definition": "bonus = the amount paid, capped at cap_micros" },
        "later_topups": { "bonus_pct": 33.333333333333336, "divisor": 3,
                          "definition": "bonus = the amount paid / divisor, rounded down" }
      }
    },
    { "program": "staking", "running": true, "starts_at": null, "expires_at": null,
      "credit_duration_secs": null, "live": true }
  ]
}
FieldTypeDescription
programstringProgram identifier — 'topup-promo' or 'staking'
runningbooleanWhether the program is switched on in the portal
starts_atstring | nullISO 8601 — when the program window opens
expires_atstring | nullISO 8601 — when the program window closes
credit_duration_secsnumber | nullHow long each issued credit lot stays valid, in seconds
livebooleanWhether the offer is issuing credit right now — running and inside the window
termsobject | undefinedThe promotion's tier arithmetic. Present on topup-promo only

Use live rather than comparing starts_at / expires_at yourself — the server folds its own clock into that field precisely so no caller has to, and running alone is not enough (a switched-on program outside its window issues nothing).

Each entry under terms carries:

FieldTypeDescription
bonus_pctnumberThe percentage to display. Derived server-side from the same rule the deposit path pays from, so no surface advertises a rate the ledger disagrees with. It is unrounded — 100 for the first tier, but 33.333333333333336 for a divisor of 3 — so round it for display
cap_microsnumber | undefinedFirst tier only — the ceiling on the bonus, in micro-USD
divisornumber | undefinedLater tiers only — the topup is divided by this, floored
definitionstringThe rule in words, server-authored

Ask what a specific account's next topup would earn

Pass account_id and the response gains your_next_topup — the authoritative answer to "is this user's next topup their first?". Prefer it over deriving first-ness from the topup list:

GET /api/partner/programs?account_id=<32-hex account id>
const res = await fetch(`${BASE}/api/partner/programs?account_id=${accountId}`);
const { programs, your_next_topup } = await res.json();

// your_next_topup: { tier: 'first' | 'later' | '', terms?: Tier, as_of: string }
// `terms` is absent whenever `tier` is '' — nothing is on offer, so there is nothing to describe.
if (your_next_topup.tier === 'first' && your_next_topup.terms) {
  console.log(`First topup — ${Math.round(your_next_topup.terms.bonus_pct)}% back`);
}

tier is '' when nothing is on offer (the promotion is not live). as_of is the server timestamp the answer was computed at.

Compute expected credit for a topup

type Tier = { bonus_pct: number; cap_micros?: number; divisor?: number };
type Terms = { first_topup: Tier; later_topups: Tier };

// Dispatch on WHICH tier you are pricing, never on which fields happen to be present:
// `cap_micros` belongs to the first tier and `divisor` to later ones today, but a tier
// carrying both would silently take the wrong branch.
function computeBonus(amountMicros: number, terms: Terms, tier: 'first' | 'later'): number {
  if (tier === 'first') {
    // 100% of what was paid, capped. Deliberately not derived from bonus_pct: the backend
    // pins the first tier to the constant `ops.FirstTopupBonusPct` (0g-pay-v2,
    // internal/ops/promo.go) and writes the payout as "the whole paid amount" rather than
    // amount*pct/100, because at 100% the multiply is a no-op that risks overflow. The
    // percentage is published FROM that constant, so it is what you SAY, never what you
    // recompute from.
    return Math.min(amountMicros, terms.first_topup.cap_micros ?? amountMicros);
  }
  // Later tiers: integer division IS the rounding rule — floor, never round.
  const { divisor } = terms.later_topups;
  return divisor ? Math.floor(amountMicros / divisor) : 0;
}

const promo = programs.find(p => p.program === 'topup-promo');
// `terms` rides on topup-promo, but guard it: a program row with no configured
// arithmetic comes back without the key.
const bonus = promo?.live && promo.terms && your_next_topup?.tier
  ? computeBonus(50_000_000, promo.terms, your_next_topup.tier)
  : 0;

your_next_topup comes from the ?account_id= request form above — a call without it returns programs alone.

Show bonus_pct in copy, but compute the figure from cap_micros / divisor — the percentage is what you say, not what the deposit path pays from. Round it before displaying: the later tier's is a true division, so 33.333333333333336 is what arrives on the wire.

Determine if a user has had their first topup

your_next_topup.tier above is the direct answer and needs no derivation. If you do want to inspect the history, call StripeClient.listTopups with source: 'stripe,chain' to exclude service-recorded rows that would otherwise produce a false "no topups yet":

const { topups } = await client.listTopups({ limit: 1, source: 'stripe,chain' });
const isFirstTopup = topups.length === 0;

Each row also carries bonus_micros and tier, so a past topup can report what it actually earned without re-deriving it.


API Reference

getApiUrl(network?)

Returns the base URL for the given network. Use this as the prefix for Partner API calls.

import { getApiUrl } from '@0gfoundation/0g-pay-sdk';

getApiUrl('testnet') // 'https://0g-pay-scan.vercel.app'
getApiUrl('mainnet') // 'https://pay.0g.ai'

Both are returned without a trailing slash — append paths as /api/v2/....

getSessionUrl(network?)

Returns the session exchange endpoint for the given network.

getSessionUrl('testnet') // 'https://0g-pay-scan.vercel.app/api/v2/session'
getSessionUrl('mainnet') // 'https://pay.0g.ai/api/v2/session'

<OGPay>

PropTypeDefaultDescription
providerEIP1193Provider—Required. EIP-1193 wallet provider (window.ethereum, wagmi, viem, …)
childrenReactNode<button>Deposit</button>Custom trigger element
methodsstring[]['crypto', 'card']Payment methods shown in the TokenFlight widget
outputAmountstring—Pre-fill the output amount in the widget
lockFromTokenbooleanfalseLock the source token to the native token
tradeTypestring'EXACT_INPUT'Trade type passed to the TokenFlight widget
logoReactNode0G logoCustom logo in the modal header
onSuccess(data) => void—Called when a TokenFlight swap completes successfully
onError(data) => void—Called when a TokenFlight swap fails
mode'user' | 'developer''user''user': fixed 0G recipient. 'developer': use recipientAddress
recipientAddressstring—Required when mode="developer". Must be a valid 0x… Ethereum address
stripeStripeConfig—When provided, adds a Card (Stripe) tab for USD top-up via Stripe

<StripeCheckoutForm>

Renders the amount picker UI standalone — useful if you want it outside the modal.

import { StripeCheckoutForm } from '@0gfoundation/0g-pay-sdk';

<StripeCheckoutForm
  config={{ network: 'testnet', sessionToken, email, successUrl, cancelUrl }}
/>
PropTypeDescription
configStripeConfigRequired. Stripe configuration object

<StripeReturn>

Polls the backend and renders the payment confirmation UI.

import { StripeReturn } from '@0gfoundation/0g-pay-sdk';

<StripeReturn
  sessionId={sessionId}
  network="testnet"
  sessionToken={sessionToken}
  onConfirmed={(creditedUsd) => { /* navigate away */ }}
/>
PropTypeDescription
sessionIdstringRequired. The session_id query param from Stripe's redirect
networkNetwork'testnet' | 'mainnet' — defaults to 'testnet'
sessionTokenstringRequired. Session token from the Pay backend
onConfirmed(creditedUsd: number) => voidCalled once when the backend confirms payment. Receives the credited amount in USD

useStripeCheckout(config)

const { createSession, loading, error } = useStripeCheckout(config: StripeConfig);
ReturnTypeDescription
createSession(amountUsd: number) => Promise<void>Creates a Stripe checkout session and redirects window.location.href to the Stripe URL
loadingbooleantrue while the API call is in flight
errorstring | nullError message if the API call failed, null otherwise

useStripeReturn(sessionId, config)

const { status, creditedMicros, availableMicrosAfter } = useStripeReturn(sessionId, {
  network: 'testnet',
  sessionToken,
});
ReturnTypeDescription
status'loading' | 'confirmed' | 'timeout'Current polling state
creditedMicrosnumberAmount credited, in micro-USD (divide by 1,000,000 for USD). Non-zero when status === 'confirmed'
availableMicrosAfternumberAccount balance after crediting, in micro-USD

Polls GET /api/v2/topup/checkout/{sessionId} up to 6 times with 3-second intervals (18-second ceiling). Stops immediately on paid: true.

StripeClient

const client = new StripeClient(config: StripeClientConfig);
MethodSignatureDescription
exchangeSession(privyToken: string) => Promise<SessionInfo>Exchange a Privy access token for a 0G Pay session token.
getBalance() => Promise<Balance>Fetch the current account balance.
createCheckoutSession(params: CreateCheckoutSessionParams) => Promise<CheckoutSession>Create a Stripe Checkout session. Returns { session_id, url } — does not redirect.
pollCheckoutStatus(sessionId: string, options?: PollCheckoutStatusOptions) => Promise<CheckoutStatus>Poll for payment confirmation. See polling behaviour below.
listTopups(params?: ListTopupsParams) => Promise<ListTopupsResult>List topup history, newest first.
getUsage(params?: GetUsageParams) => Promise<GetUsageResult>Fetch usage history.
getCredits(params?: GetCreditsParams) => Promise<GetCreditsResult>List one page of the account's credit lots. Page with cursor — see Get credits.

CreateCheckoutSessionParams

FieldTypeDescription
amountUsdnumberAmount in USD (e.g. 50 for $50). Mutually exclusive with amountMicros.
amountMicrosnumberAmount in micro-USD (e.g. 50_000_000 for $50). Mutually exclusive with amountUsd.
successUrlstringStripe redirects here after payment. Do not URL-encode {CHECKOUT_SESSION_ID}.
cancelUrlstringStripe redirects here on cancel.
emailstringPre-fill the email field in Stripe checkout.

PollCheckoutStatusOptions

FieldTypeDefaultDescription
maxAttemptsnumber6Maximum poll attempts.
intervalMsnumber3000Milliseconds between attempts.

ListTopupsParams

FieldTypeDescription
limitnumberMax number of records to return.
beforenumberPagination cursor — use next_before from the previous page.
sourcestringFilter by topup source. Use 'stripe,chain' to exclude credit-type rows when checking first-topup status.

GetUsageParams

FieldTypeDescription
limitnumberMax number of records to return.
cursorstringPagination cursor — use cursor from the previous page.

GetCreditsParams

FieldTypeDescription
limitnumberLots per page. Backend default 100, ceiling 500; a value outside 1..500 is ignored and you get 100.
cursorstringWhere to resume — pass back the previous page's next_cursor, and nothing else. A cursor you build yourself is ignored by exactly the deployments that need it.
state'granted'Ask the backend for only the lots spendable right now. A deployment that predates it ignores it silently — see Get credits.

GetCreditsResult

FieldTypeDescription
lotsCreditLot[]One page, in spend order (expires_at ASC).
next_cursorstring | null | undefinedA string means more may follow — set on any full page, so not proof. null or absent means no further page is reachable: with a short page that is the end of the list, with a full page it means this deployment cannot page and your list is partial. Never build a cursor yourself — see Get credits.

The list is paged, and a full page is never proof of the end. Never derive a money figure from a single call — Get credits has the walk.

StripeAuthError

Thrown immediately on 401/403 responses — no retries are made.

class StripeAuthError extends Error {
  status: number;  // 401 or 403
}

Thrown by: exchangeSession, getBalance, createCheckoutSession, pollCheckoutStatus, listTopups, getUsage, getCredits.

StripePollingTimeoutError

Thrown by pollCheckoutStatus when maxAttempts is exhausted and no clean HTTP response was ever received (i.e. all attempts failed due to network or server errors).

Note: If the backend responded with paid: false on every attempt, pollCheckoutStatus resolves (returns the last status) instead of throwing. This lets single-attempt reconcile calls work naturally.

class StripePollingTimeoutError extends Error {
  sessionId: string;  // the session ID being polled
  attempts: number;   // how many attempts were made
  cause?: unknown;    // the underlying fetch/HTTP error from the last attempt
}

Types

import type {
  // React components & hooks
  StripeConfig,
  OGPayProps,
  EIP1193Provider,
  // StripeClient
  StripeClientConfig,
  CreateCheckoutSessionParams,
  PollCheckoutStatusOptions,
  ListTopupsParams,
  ListTopupsResult,
  GetUsageParams,
  GetUsageResult,
  GetCreditsParams,
  GetCreditsResult,
  SessionInfo,
  Balance,
  Topup,
  UsageItem,
  CreditLot,
  // Shared
  CheckoutSession,
  CheckoutStatus,
  Network,
} from '@0gfoundation/0g-pay-sdk';
// or from '@0gfoundation/0g-pay-sdk/server' for Node.js-only projects

StripeConfig

Used by the React components (OGPay, StripeCheckoutForm) and hooks.

interface StripeConfig {
  /** 'testnet' | 'mainnet' — defaults to 'testnet'. */
  network?: Network;
  /** Session token from POST getSessionUrl(network) */
  sessionToken: string;
  /** Optional email pre-filled in Stripe checkout */
  email?: string;
  /** URL Stripe redirects to after payment. Defaults to {origin}/topup/return */
  successUrl?: string;
  /** URL Stripe redirects to on cancel. Defaults to the current page */
  cancelUrl?: string;
}

StripeClientConfig

Used by StripeClient (Node.js / server-side).

interface StripeClientConfig {
  /** 'testnet' | 'mainnet' — defaults to 'testnet'. */
  network?: Network;
  /**
   * Explicit backend URL — takes precedence over `network`.
   * Use for self-hosted proxies or direct pay-backend access.
   */
  baseUrl?: string;
  /** Session token from exchangeSession() */
  sessionToken: string;
}

SessionInfo

interface SessionInfo {
  token: string;       // 0G Pay session token — use as sessionToken
  account_id: string;
}

Balance

interface Balance {
  available_micros: number;        // the account's TOTAL, every lot, whatever service it was granted for
  spendable_micros?: number | null; // what the CALLER may actually draw — gate on this one
  debt_micros: number;
  net_micros: number;
  serve: string | null;    // "allow" = good standing; other values = service paused
  display: { message?: string; [key: string]: unknown } | null;
  as_of: string | null;    // ISO 8601 timestamp of the balance snapshot
  stop_reason: string | null;
}

serve and stop_reason are derived from spendable_micros, not from available_micros. A null spendable_micros means the field was absent from the response — treat that as untrustworthy, not as zero.

Topup

interface Topup {
  id: number;               // pass as `before` to page past this row
  source: string;           // e.g. "stripe" | "chain"
  amount_micros: number;
  reversed_micros: number;
  status: 'credited' | 'partially_reversed' | 'reversed';
  created_at: string;       // ISO 8601
  reference: string;        // Stripe checkout session ID when source === "stripe"
  bonus_micros: number;             // what the promotion granted for this topup; 0 when nothing
  tier: 'first' | 'later' | null;   // null = the promotion had no view on it, NOT "later"
}

UsageItem

interface UsageItem {
  service: string;
  event_id: string;
  charge_batch_id: string;
  sku: string;
  unit: string;
  quantity_micros: number;
  rate_micros: number;
  amount_micros: number;
  occurred_at: string;      // ISO 8601
}

CreditLot

interface CreditLot {
  lot_id: string;
  remaining_micros: number;
  granted_micros: number;
  expires_at: string;                   // ISO 8601; never null
  service?: string | null;              // null = account-wide; otherwise the service it is scoped to
  source: 'promotion' | 'staking' | 'team';                // the category to branch on
  state: 'granted' | 'spent' | 'expired' | 'revoked';      // what became of it — the field to show
  display?: string | null;              // the label to render
  depth?: number;
  parent_lot_id?: string | null;
}
sourceMeans
promotionThe top-up bonus
stakingThe $iAI staking reward
teamA campaign grant
stateMeans
grantedActive and spendable
spentDrained to zero
expiredPast its expiry — including active-but-unswept, which the user cannot spend either way
revokedCancelled: a refunded topup, or a funder pulling the credit back

display is the label: the campaign's own name for team credit, and the product's words for the two built-in programs ("Top-up bonus", "Staking reward"). Branch on source, render display — do not derive one from the other. A revoked reward stays in the list rather than disappearing, so a user whose refund clawed back a bonus can see that happen — which is why that view must be walked without state: 'granted', since the parameter filters exactly those lots out.

CheckoutSession

interface CheckoutSession {
  session_id: string; // Stripe checkout session ID (cs_...)
  url: string;        // Stripe hosted checkout URL
}

CheckoutStatus

interface CheckoutStatus {
  paid: boolean;
  credited_micros: number;       // Amount credited in micro-USD
  available_micros_after: number; // Balance after credit in micro-USD
  replayed: boolean;
}

EIP1193Provider

interface EIP1193Provider {
  request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
}

Compatible with window.ethereum, wagmi's useWalletClient, viem's wallet client, and any EIP-1193 provider.


Currency

All internal amounts use micro-USD: 1 USD = 1,000,000 micros.

const usd    = micros / 1_000_000;
const micros = Math.round(usd * 1_000_000);

StripeClient.createCheckoutSession accepts either amountUsd (float dollars) or amountMicros (integer micros). Use amountMicros when your system already works in integer cents or micros to avoid floating-point conversion.


Wallet Integration Examples

wagmi v2

import { useWalletClient } from 'wagmi';
import { OGPay } from '@0gfoundation/0g-pay-sdk';

export function DepositButton() {
  const { data: walletClient } = useWalletClient();

  if (!walletClient) return null;

  return <OGPay provider={walletClient} />;
}

viem

import { createWalletClient, custom } from 'viem';
import { OGPay } from '@0gfoundation/0g-pay-sdk';

const walletClient = createWalletClient({ transport: custom(window.ethereum) });

<OGPay provider={walletClient} />;

window.ethereum (MetaMask)

<OGPay provider={window.ethereum} />

Developer Mode

Set mode="developer" and supply recipientAddress to route TokenFlight deposits to your own address instead of the 0G recipient.

<OGPay
  provider={provider}
  mode="developer"
  recipientAddress="0xYourAddress"
/>

The SDK throws on render if mode="developer" is set without a valid 0x-prefixed 40-character hex address.


Custom Trigger

Pass any element as children to use it as the trigger instead of the default Deposit button.

<OGPay provider={provider}>
  <button className="my-custom-btn">Top Up</button>
</OGPay>

<OGPay provider={provider}>
  <img src="/pay-icon.svg" alt="Pay" style={{ cursor: 'pointer' }} />
</OGPay>

License

MIT © 0G Labs

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 20

Collected releases
VersionPublishedDirect dependenciesOpen version
0.4.0Registry latest30 Sept 202613
0.3.1-beta.1225 Sept 202613
0.3.1-beta.1124 Sept 202613
0.3.1-beta.1024 Sept 202613
0.3.1-beta.923 Sept 202613
0.3.1-beta.821 Sept 202613
0.3.1-beta.711 Sept 202613
0.3.1-beta.611 Sept 202613
0.3.1-beta.511 Sept 202613
0.3.1-beta.411 Sept 202613
0.3.1-beta.39 Sept 202613
0.3.1-beta.29 Sept 202613
0.3.1-beta.18 Sept 202613
0.3.1-beta.08 Sept 202613
0.2.12 Jul 202613
0.2.018 Jun 202613
0.1.38 Jun 202613
0.1.229 Apr 202613
0.1.127 Apr 202613
0.1.027 Apr 202613

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
0gfoundation/0g-pay-sdk

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 22:11 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:f35d82efe5a581444b2193d2e5c07819649b9318d792afebfd4cfe5dc3ec8274