402.md

@402md/checkout

Checkout widget SDK reference for vanilla JS and React

Installation

<script src="https://js.402.md/v1/checkout.js"></script>

The script exposes a global FourOhTwo object.

npm install @402md/checkout
import { createCheckout } from '@402md/checkout'

createCheckout(config)

Creates a checkout instance.

const checkout = FourOhTwo.createCheckout({
  publishableKey: 'pk_live_xxx',
  skillId: 'your-skill-id',
  amount: '49.00',
  description: 'Premium Plan - 30 days',
  type: 'subscription',
  onSuccess: (payment) => {
    console.log('Paid!', payment.txHash)
  }
})

CheckoutConfig

FieldTypeRequiredDescription
publishableKeystringYesPublishable key from 402.md dashboard
skillIdstringYesSkill ID to charge for
amountstringYesAmount in USDC (e.g. '49.00')
descriptionstringYesShown in the payment modal
typeCheckoutTypeNo'api_call' | 'subscription' | 'product' | 'service' | 'content'. Defaults to 'api_call'
metadataRecord<string, unknown>NoCustom data passed to webhooks
collectShippingbooleanNoShow shipping address form (for products)
apiBaseUrlstringNoOverride API URL. Defaults to 'https://api.402.md'
onSuccess(payment: PaymentResult) => voidNoCalled when payment is confirmed
onClose() => voidNoCalled when user closes the modal
onError(error: Error) => voidNoCalled on error

PaymentResult

interface PaymentResult {
  sessionId: string      // Checkout session ID
  txHash: string         // On-chain transaction hash
  transactionId: string  // 402.md internal transaction ID
  amount: string         // Confirmed USDC amount
  ticket?: string        // JWT ticket (subscriptions)
  orderId?: string       // Order ID (products)
}

CheckoutSession

interface CheckoutSession {
  sessionId: string    // Unique session identifier
  exactAmount: string  // Amount with precision suffix for matching
  expiresAt: string    // ISO 8601 expiration timestamp
  status: string       // CONFIRMED | SETTLED | EXPIRED | CANCELLED
}

Methods

MethodDescription
checkout.open()Opens the payment modal. Returns Promise<void>
checkout.close()Closes the modal and cleans up resources

Internal flow

  1. open() creates a checkout session via POST /api/v1/checkout/sessions
  2. The API returns a sessionId, exactAmount (with unique precision suffix), and expiresAt
  3. The modal displays a QR code, wallet address, exact amount, and countdown timer
  4. Payment detection runs in parallel:
    • WebSocket — real-time payment.confirmed events
    • Polling — checks session status every 3 seconds (fallback)
  5. On confirmation, onSuccess fires and the modal shows a success state

Unique amount matching

Each session gets a unique exact amount (e.g., 49.000037 for a $49 payment). The random suffix lets 402.md match incoming USDC transfers to the correct session without needing a unique wallet address per session.

Session TTL

Sessions expire after 15 minutes. The modal shows a countdown timer. If the session expires before payment, the EXPIRED status is shown.

Browser wallet (EIP-1193)

If the user has an injected wallet (MetaMask, Coinbase Wallet, Rainbow), the modal shows a Connect Wallet button:

  1. Requests account access via eth_requestAccounts
  2. Switches to Base (chain ID 0x2105) if needed
  3. Sends a USDC transfer to the checkout wallet
  4. Waits for on-chain confirmation

If no wallet is detected, the user can still pay via QR code or manual transfer.


@402md/checkout-react

React bindings for the checkout widget.

Installation

npm install @402md/checkout-react

<CheckoutProvider>

Wraps your app and provides the publishable key to all child checkout components.

import { CheckoutProvider } from '@402md/checkout-react'

function App() {
  return (
    <CheckoutProvider publishableKey="pk_live_xxx">
      <MyApp />
    </CheckoutProvider>
  )
}
PropTypeRequiredDescription
publishableKeystringYesPublishable key from dashboard
apiBaseUrlstringNoOverride API URL
childrenReactNodeYesChild components

useCheckout(options)

Hook for programmatic checkout control.

import { useCheckout } from '@402md/checkout-react'

function PayButton() {
  const { open, close, isOpen, isLoading, payment } = useCheckout({
    skillId: 'your-skill-id',
    amount: '49.00',
    description: 'Premium Plan',
    type: 'subscription',
    onSuccess: (payment) => {
      console.log('Paid!', payment.txHash)
    }
  })

  return <button onClick={open} disabled={isLoading}>Pay Now</button>
}

UseCheckoutOptions

FieldTypeRequiredDescription
skillIdstringYesSkill ID
amountstringYesAmount in USDC
descriptionstringYesPayment description
typeCheckoutTypeNoCommerce type
metadataRecord<string, unknown>NoCustom data
collectShippingbooleanNoCollect shipping address
onSuccess(payment: PaymentResult) => voidNoSuccess callback
onClose() => voidNoClose callback
onError(error: Error) => voidNoError callback

UseCheckoutReturn

FieldTypeDescription
open() => voidOpen the checkout modal
close() => voidClose the modal
isOpenbooleanWhether the modal is open
isLoadingbooleanLoading state during checkout
paymentPaymentResult | nullLast successful payment result

<CheckoutButton>

Pre-built button component that opens a checkout modal.

import { CheckoutButton } from '@402md/checkout-react'

<CheckoutButton
  skillId="your-skill-id"
  amount="49.00"
  description="Premium Plan"
  checkoutType="subscription"
  onSuccess={(payment) => {
    window.location.href = `/welcome?ticket=${payment.ticket}`
  }}
  className="my-button"
>
  Buy Now - $49.00 USDC
</CheckoutButton>
PropTypeRequiredDescription
skillIdstringYesSkill ID
amountstringYesAmount in USDC
descriptionstringYesPayment description
checkoutTypeCheckoutTypeNoCommerce type
metadataRecord<string, unknown>NoCustom data
collectShippingbooleanNoCollect shipping address
onSuccess(payment: PaymentResult) => voidNoSuccess callback
onClose() => voidNoClose callback
onError(error: Error) => voidNoError callback

Accepts all standard HTML <button> attributes (className, disabled, style, etc.). Shows "Loading..." while processing. Default text is Pay $${amount} if no children are provided.

Complete React example

import { CheckoutProvider, CheckoutButton } from '@402md/checkout-react'
import { useRouter } from 'next/navigation'

function PricingPage() {
  const router = useRouter()

  return (
    <CheckoutProvider publishableKey="pk_live_xxx">
      <h1>Pricing</h1>

      <CheckoutButton
        skillId="analytics-dashboard"
        amount="49.00"
        description="Analytics Pro - 30 days"
        checkoutType="subscription"
        onSuccess={(payment) => {
          router.push(`/dashboard?ticket=${payment.ticket}`)
        }}
      >
        Subscribe - $49.00/mo
      </CheckoutButton>

      <CheckoutButton
        skillId="widget-store"
        amount="29.99"
        description="Starter Widget"
        checkoutType="product"
        collectShipping
        onSuccess={(payment) => {
          router.push(`/orders/${payment.orderId}`)
        }}
      >
        Buy Widget - $29.99
      </CheckoutButton>
    </CheckoutProvider>
  )
}