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

@pafi-dev/issuer

v0.13.0

Published

Issuer backend API and services for the PAFI point token system

Readme

@pafi-dev/issuer

npm License: Apache-2.0

Backend SDK for PAFI issuer servers — claim, redeem, perp deposit, mobile prepare/submit, EIP-7702 delegation, status polling, redemption restriction enforcement, and the IssuerApiAdapter that thins issuer controllers to one line per endpoint.

Server-only. Pulls in signer wallets, HTTP clients, ledger interfaces. Don't bundle into a browser app — use @pafi-dev/trading (FE swap/quote) or @pafi-dev/core (primitives).


What's new in 0.13.0 (breaking)

Tracks @pafi-dev/[email protected]. The ReceiverConsent mint path was removed from the entire SDK chain because the deployed PointToken contract never had it — what was conceptually a "sponsored mint" is actually the path-2 MintForRequest sig-gated mint with sponsor-relayer paying gas.

| Change | Detail | |---|---| | ApiUserResponse.receiverConsentNonce removed | The /user endpoint no longer returns this field. Callers using it for signing should call the path-2 mint flow with mintRequestNonce instead. | | UserDto.receiverConsentNonce removed | Wire DTO drops the field — IssuerApiAdapter.user() response is one field smaller. | | Action required for issuer backends | Bump @pafi-dev/core to 0.10.0, remove any receiverConsentNonce field from your own DTOs / mobile API surface, drop FE code that signed ReceiverConsent typed data. |

What's new in v1.6 / 0.12.x

| Change | Detail | |---|---| | RelayService.prepareMint branches direct vs wrapper | Auto-detects MintFeeWrapper from chainId. Direct: pointToken.mint(...). Wrapper: mintFeeWrapper.mintWithFee(...) | | PTClaimHandler auto-resolves wrapper | No env config needed — getContractAddresses(chainId).mintFeeWrapper. Override via mintFeeWrapperAddress for fork tests | | PointIndexer wrapper mode | Listens to MintFeeWrapper.MintWithFee(pointToken, to, gross, net, fee) instead of Transfer(0x0→user) when wrapper is configured | | IssuerStateValidator v1.6 struct | Reads 7-field Issuer struct + queries MintingOracle.tokenCaps() for caps | | /config exposes mintFeeBpsByToken | FE can preview "you'll receive net = gross × (1-bps/10000)" before user signs | | Operator fee fallback enabled by default | Mint not blocked when subgraph has no pool yet | | PafiBackendClient error envelope fix | Reads nested error.message — exposes Pimlico AA codes instead of generic "HTTP 500" |


Requirements

  • Node.js ≥ 18
  • TypeScript ≥ 5.0
  • viem ^2.0.0 (peer)
  • @pafi-dev/core (transitive — re-exported)

Installation

pnpm add @pafi-dev/issuer @pafi-dev/core viem
# Optional ledger backend:
pnpm add @pafi-dev/issuer-postgres typeorm pg

What's in this package

@pafi-dev/issuer
├── handlers           — PTClaimHandler, PTRedeemHandler, PerpDepositHandler
├── api/IssuerApiAdapter
│                      — single class, every endpoint is one line in controller
├── api/handleMobilePrepare/Submit
│                      — mobile claim/redeem orchestrators
├── api/handleClaimStatus/handleRedeemStatus
│                      — polling helpers w/ bundler-receipt fallback
├── api/handleDelegateSubmit
│                      — EIP-7702 delegation submit (empty-batch + auth)
├── api/createSdkErrorMapper
│                      — framework-agnostic error → HTTP status mapper
├── ledger             — IPointLedger interface + InMemoryCursorStore
├── userop-store       — IPendingUserOpStore + MemoryPendingUserOpStore
├── pafi-backend       — sponsor-relayer client + relay/paymaster helpers
├── auth               — ISessionStore, AuthService (SIWE), MemorySessionStore
├── relay              — RelayService (v1.6 wrapper-aware), FeeManager
├── pools              — createSubgraphPoolsProvider, NativePtQuoter
├── policy             — IPolicyEngine + DefaultPolicyEngine
├── balance            — BalanceAggregator (off-chain + on-chain)
├── indexer            — PointIndexer (wrapper + direct mode), BurnIndexer
├── issuer-state       — IssuerStateValidator (v1.6 struct + oracle.tokenCaps)
├── redemption         — RedemptionService (per-issuer policy enforcement)
└── errors             — PafiSdkError base class for typed errors

Architecture

HTTP request (NestJS / Fastify / Express)
    ↓
Auth guard            ← issuer-specific (SIWE / Privy / NextAuth)
    ↓
Controller            ← thin: routing + DTO + auth context
    ↓
IssuerApiAdapter      ← orchestrates flows
    ↓
PTClaimHandler        ← signs MintForRequest, locks balance, builds UserOp
                        (v1.6: routes to wrapper.mintWithFee)
PTRedeemHandler       ← signs BurnRequest, reserves credit, builds UserOp
PerpDepositHandler    ← Orderly Vault via PAFI Relay
    ↓
IPointLedger          ← your DB impl (Postgres recommended)
RelayService          ← UserOp builders (auto-branches direct vs wrapper)
PafiBackendClient     ← sponsor-relayer proxy

v1.6 wrapper-mediated mint flow

[FE / mobile] → POST /claim/prepare
    ↓
[gg56] IssuerApiAdapter.claimPrepare()
    ↓
[gg56] PTClaimHandler:
    1. Lock off-chain balance (gross amount)
    2. Pre-validate via IssuerStateValidator (oracle cap check)
    3. Sign MintForRequest EIP-712 with receiver = wrapper
    4. Build UserOp callData:
       BatchExecute([
         { mintFeeWrapper, mintWithFee(pt, user, gross, deadline, sig) },
         { pointToken, transfer(pafiFeeRecipient, operatorFeePT) }
       ])
    ↓
[gg56] PafiBackendClient.requestSponsorship() → sponsor-relayer
    ↓
[sponsor-relayer] decode calldata, recognize MINT_WITH_FEE (0x5284d08b),
                  validate intent, forward to Pimlico paymaster
    ↓
[gg56] returns { userOp, typedData, userOpHash, sponsored: true|false,
                 typedDataFallback, userOpHashFallback }
    ↓
[FE / mobile] sign typedData via signTypedData_v4
    ↓
[FE / mobile] POST /claim/submit { lockId, signature, variant }
    ↓
[gg56] forwards signed UserOp to Pimlico bundler → on-chain mint
    ↓
[chain] wrapper.mintWithFee → PointToken.mint(gross to wrapper) →
        wrapper splits fee to recipients → transfers net to user
    ↓
[gg56] PointIndexer (wrapper mode) catches MintWithFee event →
       deduct off-chain balance, lock → MINTED

Quick start (NestJS)

Minimal reference at examples/nestjs-issuer/ — full working backend.

1. Wire IssuerApiAdapter

import { Provider } from "@nestjs/common";
import {
  IssuerApiAdapter,
  IssuerStateValidator,
  PTClaimHandler,
  PTRedeemHandler,
  PerpDepositHandler,
  MemoryPendingUserOpStore,
  type IssuerService,
} from "@pafi-dev/issuer";
import { PostgresPointLedger } from "@pafi-dev/issuer-postgres";
import { getContractAddresses } from "@pafi-dev/core";

export const issuerApiAdapterProvider: Provider = {
  provide: ISSUER_API_ADAPTER,
  useFactory: (issuerService, provider, walletClient, dataSource, config) => {
    const ledger = new PostgresPointLedger(dataSource);
    const { issuerRegistry, batchExecutor } = getContractAddresses(8453);
    const chainId = config.get<number>("CHAIN_ID");
    const pointToken = config.get<`0x${string}`>("POINT_TOKEN_ADDRESS");

    return new IssuerApiAdapter({
      issuerService,
      ledger,
      provider,
      issuerSignerWallet: walletClient,
      pafiIssuerId: config.get("PAFI_ISSUER_ID"),

      ptClaimHandler: new PTClaimHandler({
        ledger,
        relayService: issuerService.relay,
        provider,
        issuerSignerWallet: walletClient,
        pointTokenDomainName: config.get("POINT_TOKEN_DOMAIN_NAME"),
        feeService: issuerService.fee,
        issuerStateValidator: new IssuerStateValidator(provider, issuerRegistry),
        // mintFeeWrapperAddress: optional override; SDK auto-resolves from chainId
      }),

      ptRedeemHandler: new PTRedeemHandler({
        ledger,
        relayService: issuerService.relay,
        provider,
        pointTokenAddress: pointToken,
        batchExecutorAddress: batchExecutor,
        chainId,
        domain: { name: config.get("POINT_TOKEN_DOMAIN_NAME"), verifyingContract: pointToken },
        burnerSignerWallet: walletClient,
        feeService: issuerService.fee,
      }),

      perpHandler: new PerpDepositHandler({
        provider, feeService: issuerService.fee, pointTokenAddress: pointToken,
      }),

      pendingUserOpStore: new MemoryPendingUserOpStore(),
      pafiBackendClient,  // optional — sponsor-relayer client for sponsored flows
    });
  },
  inject: [...],
};

Skip handlers you don't need — adapter throws clear " not wired" if the corresponding method is called.

2. Slim controller

@Controller()
export class IssuerController {
  constructor(@Inject(ISSUER_API_ADAPTER) private api: IssuerApiAdapter) {}

  @Post("claim/prepare")
  @UseGuards(JwtGuard)
  async claimPrepare(@User() user: AuthContext, @Body() body) {
    const [aaNonce, mintRequestNonce] = await Promise.all([
      fetchAaNonce(this.provider, user.userAddress),
      fetchMintRequestNonce(this.provider, body.pointTokenAddress, user.userAddress),
    ]);
    return wrap(() => this.api.claimPrepare({
      authenticatedAddress: user.userAddress,
      chainId: body.chainId,
      pointTokenAddress: body.pointTokenAddress,
      amount: BigInt(body.amount),
      aaNonce, mintRequestNonce,
    }));
  }
}

3. Wire error mapper

import { createSdkErrorMapper, type SdkErrorBody } from "@pafi-dev/issuer";

const sdkErrorMapper = createSdkErrorMapper({
  notFound:           (b) => new NotFoundException(b),
  forbidden:          (b) => new ForbiddenException(b),
  unprocessable:      (b) => new UnprocessableEntityException(b),
  serviceUnavailable: (b) => new ServiceUnavailableException(b),
});

async function wrap<T>(fn: () => Promise<T>): Promise<T> {
  try { return await fn(); }
  catch (err) { sdkErrorMapper(err); }
}

IssuerApiAdapter methods

| Method | HTTP route | Notes | | --- | --- | --- | | pools(authedAddr, chainId, pointToken) | GET /pools | | | user(authedAddr, chainId, userAddr, pointToken) | GET /user | | | config(chainId) | GET /config | v1.6: includes mintFeeBpsByToken + contracts.mintFeeWrapper | | claim({ ...nonces }) | POST /claim (web — sync calls[]) | | | redeem({ amount, aaNonce, ... }) | POST /redeem | | | perpDeposit({ amount, brokerId, aaNonce, ... }) | POST /perp-deposit | | | claimPrepare(...) / claimSubmit(...) | Mobile claim flow | v1.6 wrapper-aware | | redeemPrepare(...) / redeemSubmit(...) | Mobile redeem flow | | | claimStatus(authedAddr, lockId) / redeemStatus(...) | Status polling | | | delegateStatus(authedAddr, chainId) | EIP-7702 — check delegation | | | delegatePrepare(authedAddr, chainId) | EIP-7702 — build auth hash | | | delegateSubmit({ authSig, ... }) | EIP-7702 — relay empty-batch | |


Redemption restriction (v0.10+)

Per-issuer policy enforced at /redeem/prepare time. Fetched from PAFI issuer-api with 5-min cache + fail-open default.

import { RedemptionService, PolicyProvider } from "@pafi-dev/issuer";
import { PostgresRedemptionHistoryStore } from "@pafi-dev/issuer-postgres";

const redemption = new RedemptionService({
  policyProvider: new PolicyProvider({
    chainId: 8453,
    issuerId: "gg56",
    apiKey: process.env.PAFI_API_KEY,
  }),
  historyStore: new PostgresRedemptionHistoryStore(dataSource),
});

Wire to createIssuerService({ redemption: {...} }). Adapter exposes /redemption/preview + /redemption/evaluate automatically.


Mobile prepare/submit flow

// 1. mobile → POST /claim/prepare → backend runs:
const prepared = await api.claimPrepare({
  authenticatedAddress, chainId, pointTokenAddress, amount, aaNonce, mintRequestNonce,
});
// returns: {
//   lockId, userOpHash, typedData,
//   userOpHashFallback, typedDataFallback,
//   sponsored,           // true if paymaster signed; false → use fallback
//   needsDelegation,     // true if user EOA not delegated yet
//   feeAmount, signatureDeadline, expiresInSeconds,
// }

// 2. mobile signs typedData via Privy/MetaMask signTypedData_v4
//    (NOT personal_sign — Pimlico Simple7702Account does raw ecrecover)
//    If `sponsored: false`, sign `typedDataFallback` instead

// 3. mobile → POST /claim/submit:
const result = await api.claimSubmit({
  authenticatedAddress, lockId, signature,
  variant: prepared.sponsored ? "sponsored" : "fallback",
});
// returns: { userOpHash } — bundler hash for status polling

handleMobileSubmit enforces ownership: entry.sender === authenticatedAddress. Lock has 15-min TTL.


PointIndexer modes

// Default — auto-resolves wrapper from chainId
const service = createIssuerService({
  chainId: 8453,
  // ...
  indexer: { autoStart: true, pollIntervalMs: 5000 },
});

// Indexer listens to:
//   - mode wrapper:  MintFeeWrapper.MintWithFee filtered by pointToken
//                    (when wrapper is configured at chainId — v1.6 default)
//   - mode direct:   PointToken.Transfer(0x0 → user) (legacy / no wrapper)

Override for fork tests:

indexer: { mintFeeWrapperAddress: "0x000...dead" }  // force direct mode

Error classes

All SDK errors inherit PafiSdkError. Subclasses + HTTP mapping:

| Error | httpStatus | safeToRetry | | --- | --- | --- | | PendingUserOpNotFoundError | not_found | false | | PendingUserOpForbiddenError | forbidden | false | | LockNotFoundError | not_found | false | | IssuerStateError (ISSUER_NOT_REGISTERED / ISSUER_INACTIVE / MINT_CAP_EXCEEDED) | unprocessable | true if cap exceeded | | PerpDepositError | unprocessable | true if RELAY_FEE_EXCEEDS_AMOUNT | | PTClaimError, PTRedeemError | unprocessable | false | | BundlerNotConfiguredError | service_unavailable | false | | BundlerRejectedError | unprocessable | false | | RedemptionPolicyError (REDEMPTION_DENIED / RATE_LIMIT_EXCEEDED / ...) | unprocessable | varies |


v1.5 → v1.6 migration checklist

  1. pnpm add @pafi-dev/issuer@latest @pafi-dev/core@latest (≥ 0.12.7 + ≥ 0.9.6)
  2. Update env: drop MINT_FEE_WRAPPER_ADDRESS if previously set (SDK auto-resolves from chainId)
  3. Re-deploy your issuer backend — RelayService now branches based on wrapper presence; calldata changes from mint(...) to mintWithFee(...)
  4. Verify on-chain prerequisites (PAFI ops responsibility):
    • IssuerRegistry.addIssuer(...) cascade configures wrapper recipients
    • MintingOracle.registerToken(pointToken, issuer) sets cap
    • PointToken.addMinter(signerAddress) whitelists your signer
  5. Indexer will pick up MintWithFee events automatically. If you wrote custom Transfer-event consumers, switch to MintWithFee.
  6. /config response now includes mintFeeBpsByToken — FE can preview fee without round-tripping getMintFeeBps() separately.

References

License

Apache-2.0