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

@menakalakshan/poly-pay

v1.6.0

Published

Polygon ERC-20 Payment Receiver SDK — self-custodial token monitoring with memo-encoded user identification, webhook ingestion, and MongoDB persistence

Readme

poly-pay

Polygon USDC Payment Receiver SDK — a self-contained Node.js library for accepting USDC payments on Polygon PoS with memo-encoded user identification, webhook-first event ingestion, MongoDB persistence, and automatic catch-up scanning.

Features

  • Self-custodial — monitors your own receiver address, no custodial gateway
  • Memo encoding — embeds user IDs in ERC-20 calldata for payment attribution
  • Dual provider support — Alchemy (webhook + WebSocket) or Infura (WebSocket-only)
  • Zero RPC polling — push-only ingestion; getLogs only runs at startup for catch-up
  • Idempotent — MongoDB unique index on txHash prevents duplicate records
  • Catch-up scanner — recovers missed payments after any downtime, no matter how long
  • TypeScript-first — full .d.ts declarations, dual CJS + ESM output
  • Frontend helper — tree-shakeable poly-pay/client for encoding transfers in the browser

Installation

npm install poly-pay

Requires Node.js >= 18 and MongoDB.

Quick Start

Server

import { PolyPay } from 'poly-pay';
import express from 'express';

const app = express();
app.use(express.json());

const pay = new PolyPay({
  provider:         'alchemy',
  receiverAddress:  '0xYOUR_RECEIVER_WALLET',
  mongoUri:         process.env.MONGO_URI,

  // Alchemy mode
  alchemyApiKey:    process.env.ALCHEMY_API_KEY,
  alchemySigningKey:process.env.ALCHEMY_SIGNING_KEY,

  // Optional user resolver — called only when a memo is present
  resolveUser: async (userId) => {
    return await User.findById(userId);
  },
});

await pay.start();

// All transfers to your address are captured — with or without memo
pay.on('payment', ({ txHash, userId, memo, isMemoAvailable, amount, tokenSymbol, from }) => {
  if (isMemoAvailable) {
    console.log(`${userId} paid ${amount} ${tokenSymbol} (memo: ${memo})`);
  } else {
    console.log(`Anonymous payment: ${amount} ${tokenSymbol} from ${from}`);
  }
});

// Mount webhook endpoint (Alchemy mode only)
app.use('/webhooks/poly-pay', pay.webhookRouter());
app.listen(3000);

Frontend

import { encodeTransfer, USDC_POLYGON } from 'poly-pay/client';

const data = encodeTransfer({
  to:     '0xYOUR_RECEIVER_WALLET',
  amount: '10.00',
  memo:   currentUser.id,
});

await window.ethereum.request({
  method: 'eth_sendTransaction',
  params: [{ to: USDC_POLYGON, data }],
});

Custom ERC-20 Token

poly-pay works with any ERC-20 token on Polygon, not just USDC. Pass the contract address and decimals:

// Example: Accept DAI payments
const pay = new PolyPay({
  provider:       'alchemy',
  receiverAddress:'0xYOUR_WALLET',
  mongoUri:       process.env.MONGO_URI,
  alchemyApiKey:  process.env.ALCHEMY_API_KEY,
  alchemySigningKey: process.env.ALCHEMY_SIGNING_KEY,

  // Custom token config
  tokenAddress:  '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', // DAI on Polygon
  tokenDecimals: 18,
  tokenSymbol:   'DAI',

  resolveUser: async (userId) => User.findById(userId),
});

pay.on('payment', ({ txHash, userId, amount, tokenSymbol }) => {
  console.log(`${userId} paid ${amount} ${tokenSymbol}`);
  // → "usr_123 paid 50.0 DAI"
});

Frontend — pass decimals to match:

import { encodeTransfer } from 'poly-pay/client';

const DAI_POLYGON = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063';

const data = encodeTransfer({
  to:       '0xYOUR_WALLET',
  amount:   '50.00',
  memo:     currentUser.id,
  decimals: 18,  // DAI has 18 decimals
});

await window.ethereum.request({
  method: 'eth_sendTransaction',
  params: [{ to: DAI_POLYGON, data }],
});

If you omit tokenAddress / tokenDecimals / tokenSymbol, the library defaults to native USDC (6 decimals) — fully backwards compatible.

Configuration

Constructor Options

| Option | Type | Required | Default | Description | |---|---|---|---|---| | provider | 'alchemy' \| 'infura' | Yes | — | Provider mode | | receiverAddress | string | Yes | — | Your wallet address to monitor | | mongoUri | string | Yes | — | MongoDB connection string | | alchemyApiKey | string | Alchemy | — | Alchemy API key | | alchemySigningKey | string | Alchemy | — | Webhook HMAC signing key | | alchemyWebhookId | string | No | — | Alchemy webhook ID | | infuraApiKey | string | Infura | — | Infura API key | | tokenAddress | string | No | Native USDC | ERC-20 contract address to monitor | | tokenDecimals | number | No | 6 | Token decimal places (e.g. 18 for DAI) | | tokenSymbol | string | No | 'USDC' | Token symbol for labelling | | network | 'polygon' \| 'polygon-amoy' | No | 'polygon' | Target network | | memoEncoding | 'calldata' \| 'event' | No | 'calldata' | Memo encoding strategy | | catchUpOnStart | boolean | No | true | Run catch-up scan on start | | blockBatchSize | bigint | No | 2000n | Blocks per catch-up batch | | startBlock | bigint | No | — | Starting block if no sync state exists | | resolveUser | (id: string) => Promise<any> | Yes | — | User lookup callback |

Environment Variables

# Provider (pick one)
PROVIDER=alchemy
ALCHEMY_API_KEY=your_key
ALCHEMY_SIGNING_KEY=whsec_xxx
# PROVIDER=infura
# INFURA_API_KEY=your_key

# MongoDB
MONGO_URI=mongodb://localhost:27017/polypay

# Receiver
RECEIVER_ADDRESS=0xYOUR_WALLET

# Optional
NETWORK=polygon
CATCH_UP_ON_START=true

Provider Modes

Alchemy (recommended for production)

Uses Alchemy's address-activity webhook as the primary ingestion path. A WebSocket listener runs concurrently as a fallback. Alchemy retries webhook delivery up to 5 times if your server is temporarily down.

new PolyPay({
  provider: 'alchemy',
  alchemyApiKey: '...',
  alchemySigningKey: '...',
  // ...
});

Requires webhook setup in the Alchemy dashboard — see Webhook Setup below.

Infura (simpler setup)

Uses a persistent WebSocket subscription as the sole live-ingestion path. No webhook endpoint is exposed, eliminating that attack surface. The catch-up scanner provides the same recovery guarantee.

new PolyPay({
  provider: 'infura',
  infuraApiKey: '...',
  // ...
});

API

PolyPay class

pay.start(): Promise<void>

Connects to MongoDB, starts the WebSocket listener, and runs the catch-up scan (if enabled).

pay.stop(): Promise<void>

Disconnects the WebSocket, closes the MongoDB connection.

pay.webhookRouter(): express.Router

Returns an Express Router to mount at your webhook endpoint. Alchemy mode only.

pay.catchUp(options?): Promise<void>

Manually trigger a catch-up scan.

await pay.catchUp({ fromBlock: 58_000_000n });

pay.isRunning(): boolean

Returns whether the instance is currently active.

Events

pay.on('payment', ({
  txHash,          // transaction hash
  userId,          // string | null — decoded user ID from memo
  memo,            // string | null — raw memo value
  isMemoAvailable, // boolean — whether the tx contained a memo
  amount,          // formatted token amount
  tokenSymbol,     // e.g. 'USDC', 'DAI'
  from,            // sender address
  blockNumber,     // block number
  source,          // 'webhook' | 'websocket' | 'catchup'
}) => {});
pay.on('error', (error) => {});
pay.on('reconnecting', (attempt) => {});
pay.on('started', () => {});
pay.on('stopped', () => {});

encodeTransfer(params) (client)

import { encodeTransfer } from 'poly-pay/client';

const calldata = encodeTransfer({
  to:     '0xRECEIVER',
  amount: '25.50',       // USDC as string, or bigint in raw units
  memo:   'user_123',
});

MongoDB Schema

payments collection

| Field | Type | Description | |---|---|---| | txHash | String (unique) | Transaction hash — idempotency key | | userId | String | null (indexed) | Decoded user ID from memo, or null | | memo | String | null | Decoded memo value, or null | | isMemoAvailable | Boolean | Whether the tx contained a memo | | fromAddress | String | Sender wallet address | | amount | String | Formatted token amount as decimal string | | tokenAddress | String (indexed) | ERC-20 contract address | | tokenSymbol | String | Token symbol (e.g. 'USDC', 'DAI') | | tokenDecimals | Number | Token decimal places | | blockNumber | Number | Block the tx was included in | | network | String | 'polygon' or 'polygon-amoy' | | status | String | 'confirmed' | | memoRaw | String | Raw hex input data | | source | String | 'webhook', 'websocket', or 'catchup' | | createdAt | Date | When the record was created |

sync_state collection

Singleton document tracking the last processed block for the catch-up scanner.

Reliability

  • Idempotency: Unique txHash index — safe for the same event to arrive from webhook, WebSocket, and catch-up simultaneously
  • Catch-up on startup: Scans from the last checkpoint to the current block in 2,000-block batches
  • Checkpoint safety: $max operator ensures the block pointer never moves backwards
  • Interrupted scan resume: Checkpoints after each batch, so a killed process resumes from where it left off
  • WebSocket reconnection: Exponential backoff (1s → 2s → 4s → ... → 30s max)

Webhook Setup

For Alchemy mode:

  1. Go to the Alchemy dashboard, select your Polygon app
  2. Navigate to Notify → Create Webhook
  3. Choose Address Activity
  4. Enter your receiver address and webhook URL: https://yourdomain.com/webhooks/poly-pay
  5. Copy the Signing KeyALCHEMY_SIGNING_KEY
  6. Copy the Webhook IDALCHEMY_WEBHOOK_ID

USDC Contract Addresses

| Network | Address | |---|---| | Polygon mainnet (native USDC) | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | | Polygon mainnet (bridged USDC.e) | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 | | Polygon Amoy testnet | 0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582 |

Development

# Install dependencies
npm install

# Type-check
npm run typecheck

# Run tests
npm test

# Build (CJS + ESM + .d.ts)
npm run build

# Start MongoDB for local dev
docker compose up -d

License

MIT