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

razorpay-agentkit

v1.0.0

Published

Server-side SDK for integrating an AgentKit trust kernel: agent identity, signed intents, quotes and consent.

Downloads

168

Readme

razorpay-agentkit

Server-side SDK for integrating an AgentKit trust kernel.

An agent can hold a signing key without ever holding a payment credential. This package gives your backend the three things that are genuinely hard to get right on your own: canonical bytes, Ed25519 intent signing, and the separation between the credential you give an agent and the one you never do.

git clone https://github.com/Abhijeet212004/razorpay_hackathon.git
npm install ./razorpay_hackathon/packages/merchant

Requires Node 18 or newer. Zero dependencies.

Not on npm yet. The package is complete, tested and named razorpay-agentkit, but it has not been published. Install from the path above, or add "razorpay-agentkit": "file:../packages/merchant" to your package.json — which is exactly how the storefront and the adapter in this repository consume it.

Why not just call the HTTP API

You can, and the API is documented. But three details are unforgiving, and all three fail the same way: the kernel answers INT-001, which means this signature does not match this payload, and deliberately says no more than that. A signature check that explained itself would be an oracle for forging one.

  1. Canonical JSON. Signatures cover RFC 8785 bytes. Key order, escaping and number formatting all have to match the kernel exactly.
  2. Money is a string. amount_paise is signed as a decimal string, never a JSON number. Paise exceed float precision long before they exceed a realistic basket, and a float that rounds is a price that silently changed.
  3. The signing payload is a closed set. Ten named fields for an intent, nine for a quote. An extra key changes the bytes; so does a missing one.

This package is tested against the kernel's own implementation, byte for byte, so these stay in step.

Quick start

const { AgentKit } = require("razorpay-agentkit");

const kit = new AgentKit({
  baseUrl: "https://kernel.yourshop.in",
  apiKey: process.env.AGENTKIT_API_KEY,
});

const keys = AgentKit.generateKeyPair();
const { agentId } = await kit.registerAgent({ name: "Shop Assistant", publicKey: keys.publicKey });

// Persist agentId and keys.privateKey. See "Identity is not a cache" below.

const signedQuote = await kit.quote({ mandateId, items: [{ sku: "rice-5kg", quantity: 1 }] });

const agent = kit.agent({ agentId, privateKey: keys.privateKey });
const decision = await agent.checkout({ mandateId, signedQuote, rationale: "weekly staples" });

if (decision.verdict === "ALLOW") {
  // decision.pay_url, decision.audit_url
} else if (decision.verdict === "STEP_UP") {
  // send the shopper to decision.approval_url
}

Two credentials, two doors

They are not interchangeable, and mixing them up is the one mistake with a real blast radius.

| Option | Header | Who holds it | What it opens | | --- | --- | --- | --- | | apiKey | x-agentkit-key | The agent | Catalog, quotes, checkout, orders | | fulfilToken | x-agentkit-token | Your backend only | Binding a consent request to a real customer |

fulfilToken also signs authorization tokens. Never ship it to an agent. The SDK enforces the split: a call through the agent door never carries the merchant token, and the reverse is also true. There is a test for exactly this.

Identity is not a cache

A mandate is granted to an agent id, which is derived from a public key. If you generate a fresh keypair on each deploy, every mandate a shopper ever granted you is orphaned on the next restart, and every purchase is refused with MND-001 and nothing on screen to explain why.

Store agentId and the private key in your database and load them at boot.

The private key is a signing key, never a payment credential. The worst an attacker who steals it can do is propose purchases, and those still have to pass the mandate and every policy rule.

Refusals are answers

const { AgentKitRefusal, AgentKitError } = require("razorpay-agentkit");

try {
  await agent.checkout({ mandateId, signedQuote });
} catch (err) {
  if (err instanceof AgentKitRefusal) {
    // The kernel decided. err.reasonCode is stable and safe to branch on.
    if (err.reasonCode === "CAP-001") notifyShopperCapReached();
  } else if (err instanceof AgentKitError) {
    // The transport failed. Nothing was decided; retrying is safe.
  }
}

A denied purchase and an unreachable kernel are never the same object. Reason codes are stable and read identically here, in the ledger and in the dashboard.

Common codes: MND-001 no live mandate, CAP-001 cap exhausted, SEC-002 unknown agent, INT-001 signature mismatch, INT-003 amount or basket differs from the quote.

Consent

The agent supplies neither the customer nor the address, and never learns either. It is told a mandate reference; your backend resolves the address at fulfilment time.

const consent = await kit.requestConsent({
  agentId,
  contact: shopper.phone,
  requestedScope: { merchants: ["mch_yourshop"], categories: ["groceries"], currency: "INR" },
  limits: {
    per_transaction_paise: "500000",
    cumulative_paise: "1500000",
    silent_threshold_paise: "50000",   // above this, the shopper is asked
    velocity_per_hour: 3,
  },
});
// Send the shopper to consent.consent_url

To bind that request to a real customer, mint a token from the merchant door and let the shopper's own browser carry it:

const token = kit.authorizationToken({
  requestRef: consent.request_ref,
  customerRef: user.id,
  fulfilmentRef: address.id,
  displayName: user.name,
  displayAddress: address.oneLine,
});

The display fields are the point. You cannot prove to the kernel that this customer id is this person, because it is your namespace and opaque to them. So the kernel shows the shopper the name and address you claim and lets them decline if it is not theirs. The one party who can check is the one asked to.

The token is ref-bound and expires in ten minutes, so it cannot be replayed into another session.

The signing key is the SHA-256 of your fulfil token, not the token itself. The kernel stores only that hash, so it can verify your handoffs without ever holding your token in a recoverable form, and every merchant ends up with a distinct key. The SDK does this for you.

API

new AgentKit({ baseUrl, apiKey, fulfilToken, timeoutMs, fetch }) fetch is injectable for testing. timeoutMs defaults to 15000.

AgentKit.generateKeyPair(){ publicKey, privateKey } as raw 32-byte Buffers.

kit.registerAgent({ name, publicKey }){ agentId }. Identity, never authority.

kit.agent({ agentId, privateKey }) — an Agent that can sign. No network call.

kit.quote({ mandateId, items }){ quote, kid, signature }.

agent.checkout({ mandateId, signedQuote, rationale, intentTtlMs }) — a Decision. Amount and basket are taken from the quote, never from the caller.

kit.requestConsent(...), kit.consentStatus(ref), kit.bindConsent(ref, token), kit.authorizationToken(...)

kit.audit(intentId) — the public record of one decision: what happened to this intent, in order, with the hash that fixes each entry in the chain, and chain_intact. Needs no credential, so the same call works from a support tool or a shopper's own page.

kit.searchCatalog({ mandateId, query }), kit.catalogItem(sku), kit.mandate(id), kit.orderStatus(b), kit.orderHistory(b), kit.cancelOrder(b), kit.reorder(b)

kit.tools(), kit.manifest(), kit.health()

Escape hatch: kit.request(method, path, { body, door, headers }) for anything not wrapped above.

Also exported for callers who transport signatures themselves: canonicalise, canonicalBytes, signPayload, verifyPayload, intentSigningPayload, quoteSigningPayload, paiseToCanonical.

Tests

npm test                      # this package, no network
npx vitest run tests/sdk/     # from the repo root: byte-for-byte against the kernel

Licence

Apache-2.0