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

upi-402

v0.1.0

Published

Open HTTP 402 payment standard for UPI

Readme

upi-402

Open HTTP 402 payment standard for UPI.

x402 brought HTTP 402 to crypto. L402 brought it to Lightning. This brings it to UPI — the world's largest real-time payment network. 23 billion transactions/month. Open standard. PA-agnostic.

Install

npm install upi-402

Quick start

Server (5 lines):

import express from 'express';
import { upi402 } from 'upi-402';

const app = express();
app.get('/api/data', upi402({ vpa: 'merchant@ybl', amount: 100 }), (req, res) => {
  res.json({ secret: 'paid content', receipt: req.upi402.receipt });
});
app.listen(3000);

Client (3 lines):

import { upi402Fetch } from 'upi-402/client';

const res = await upi402Fetch('http://localhost:3000/api/data', { mandateRef: 'TEST-001' });
console.log(await res.json()); // { secret: 'paid content', receipt: {...} }

Run both. The client gets a 402, retries with the mandate header, gets 200 + receipt. Zero configuration — mock mode is the default.

The protocol

1. Server returns 402

HTTP/1.1 402 Payment Required
X-UPI-402-Version: 1

{
  "upi402": 1,
  "payee": { "vpa": "merchant@ybl", "name": "Example API" },
  "payment": { "amount": 500, "currency": "INR", "description": "API access" },
  "mandate": { "required": true, "maxAmount": 5000, "frequency": "DAILY" }
}

2. Client retries with mandate

GET /api/data HTTP/1.1
Authorization: UPI-Mandate umn=ABCD1234567890&txnRef=TXN789

3. Server verifies and responds

HTTP/1.1 200 OK
X-UPI-402-Receipt: {"txnId":"UPI123","amount":500,"timestamp":"2026-06-13T14:32:18Z","umn":"ABCD1234567890"}

{ "data": "..." }

See SPEC.md for the full protocol specification.

Server middleware API

import { upi402 } from 'upi-402';

app.get('/api/data', upi402(options), handler);

Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | vpa | string | yes | — | Merchant UPI VPA | | name | string | no | same as vpa | Merchant display name | | amount | number | yes | — | Amount in INR | | currency | string | no | "INR" | Currency code | | description | string | no | — | Payment description | | mandate | object | no | { required: true } | Mandate configuration | | requireSignature | boolean | no | false | Reject unsigned requests | | verify | function | no | mock verifier | Verification function |

With a real verifier

import { razorpayVerifier } from 'upi-402/verifiers/razorpay';

app.get('/api/data', upi402({
  vpa: 'merchant@ybl',
  amount: 500,
  verify: razorpayVerifier({
    keyId: process.env.RAZORPAY_KEY_ID,
    keySecret: process.env.RAZORPAY_KEY_SECRET
  })
}), handler);

With a custom verifier

app.get('/api/data', upi402({
  vpa: 'merchant@ybl',
  amount: 500,
  verify: async (mandateRef, amount, txnRef) => {
    const result = await yourPA.executeDebit(mandateRef, amount);
    return { success: result.ok, txnId: result.txnId };
  }
}), handler);

Verify function signature

type VerifyFunction = (
  mandateRef: string,
  amount: number,
  txnRef: string,
  metadata?: Record<string, any>
) => Promise<{ success: boolean; txnId?: string; error?: string }>;

Client API

import { upi402Fetch } from 'upi-402/client';

const res = await upi402Fetch(url, options);

Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | mandateRef | string | yes | — | UPI Unique Mandate Number | | txnRef | string | no | auto-generated | Transaction reference for idempotency | | privateKey | string | no | auto-generated | Ed25519 private key (base64). Auto-generates a keypair per session if omitted. | | maxRetries | number | no | 1 | Max retry attempts after 402 | | onPaymentRequired | function | no | — | Called when 402 is received | | onPaymentComplete | function | no | — | Called after successful payment |

The client wraps fetch with 402 retry logic. When the server returns a paymentId in the 402 response, the client automatically signs the payment agreement with Ed25519 before retrying. The response has a .upi402Receipt property with the payment receipt.

Error handling

import { upi402Fetch, UPI402PaymentError } from 'upi-402/client';

try {
  const res = await upi402Fetch(url, { mandateRef: 'MANDATE-001' });
} catch (err) {
  if (err instanceof UPI402PaymentError) {
    console.log(err.details.error);    // "mandate_expired", "debit_failed", etc.
    console.log(err.details.payee.vpa); // merchant VPA
  }
}

Verifiers

| Verifier | Status | Notes | |----------|--------|-------| | Mock | Tested | Full 402 -> sign -> 202 -> 200 flow, all tests passing | | Razorpay | Implemented | Targets POST /v1/payments/create/recurring (instant charge). Requires S2S enablement on Razorpay account |

See PA_RESEARCH.md for why other PAs were evaluated and excluded.

The verify interface is simple — implement one async function returning { success, pending, txnId }. PRs adding verifiers for PAs that support instant charge (no scheduling) are welcome.

Agent-side code (upi-402/client) has zero dependencies. Verifier code is server-side only and tree-shakeable.

How UPI mandates work

UPI is a single real-time payment network operated by NPCI (National Payments Corporation of India). PhonePe, Google Pay, Paytm, Razorpay, bank apps, and even *99# USSD are all interfaces into the same system.

A mandate (also called autopay/recurring authorization) is a pre-approved debit permission. The user sets it up through any UPI app — the standard doesn't care which. The mandate gets a Unique Mandate Number (UMN) at the NPCI level. Any Payment Aggregator (PA) can then execute debits against that UMN within the approved limits.

This protocol uses mandates because:

  • The agent doesn't need to be present for each payment
  • The user approves once, the agent can pay within limits
  • It works exactly like how subscription services use UPI autopay today

Payment scoping (overcharge protection)

A malicious merchant could debit more than the agreed amount from a UPI mandate. NPCI enforces mandate-level limits but not per-transaction agreements. upi-402 solves this with decentralized payment scoping — no central authority required.

How it works

  1. Server returns 402 with a unique paymentId
  2. Client signs paymentId:amount:merchantVpa:timestamp with Ed25519
  3. Client sends signature + public key in the Authorization header
  4. Middleware verifies the signature and passes the signed amount to the verifier
  5. If the server tries to debit a different amount, the middleware blocks it
Client signs: "pay-uuid:500:merchant@ybl:1718300000"
Server receives: signature proves client agreed to exactly ₹500
Middleware: passes ₹500 to verifier, regardless of server config

What this prevents

  • Overcharge: merchant debits ₹500 when client agreed to ₹100 — middleware blocks
  • Replay: merchant reuses a paymentId — middleware blocks (single-use)
  • Tampering: man-in-middle changes the amount — signature verification fails

What this doesn't prevent

  • Merchant modifies the middleware source code — but client holds signed evidence for dispute
  • Same trust model as HTTPS: you trust the software, disputes handle bad actors

Backward compatibility

Signing is opt-in. Unsigned requests still work by default. Set requireSignature: true on the middleware to enforce signed requests.

Future: Agent Identity

The Authorization header is designed to be extensible. v2 may add:

Authorization: UPI-Mandate umn=ABCD1234&txnRef=TXN789&agent=did:web:myagent.dev&grant=eyJhbG...

v1 ships without identity fields. The header parser ignores unknown fields, so v1 servers work with v2 clients.

Use with PayRouter

import { upi402Fetch } from 'upi-402/client';
// Plug into PayRouter's UPI adapter — one import, handles the 402 handshake.

Contributing

Especially wanted:

  • Testing PhonePe, Cashfree, and Stripe verifiers with real credentials
  • Additional PA verifiers (Paytm, PayU, etc.)
  • Framework adapters beyond Express (Hono, Fastify, etc.)

License

Apache 2.0