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

@clawallex/sdk

v1.0.2

Published

Clawallex Payment API TypeScript SDK

Downloads

353

Readme

@clawallex/sdk

TypeScript/Node.js SDK for the Clawallex Payment API.

Installation

npm install @clawallex/sdk

Quick Start

import { ClawallexSDK } from "@clawallex/sdk";

// First run — SDK auto-resolves client_id via whoami/bootstrap
const client = await ClawallexSDK.create({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  baseUrl: "https://api.clawallex.com",
});

// ⬇️ Persist client.clientId to your config/database/env
// e.g. "ca_8f0d2c3e5a1b4c7d"
console.log(client.clientId);

// Subsequent runs — pass the stored client_id to skip network calls
const client2 = await ClawallexSDK.create({
  apiKey: "your-api-key",
  apiSecret: "your-api-secret",
  baseUrl: "https://api.clawallex.com",
  clientId: "ca_8f0d2c3e5a1b4c7d", // the value you persisted
});

Client ID

client_id is your application's stable identity on Clawallex, separate from the API Key.

  • You can rotate API Keys (revoke old, create new) without losing access to existing cards and transactions — just keep using the same client_id
  • When a new API Key sends its first request with an existing client_id, the server auto-binds the new key to that identity
  • Once bound, a client_id cannot be changed for that API Key (TOFU — Trust On First Use)
  • Cards and transactions are isolated by client_id — different client_ids cannot see each other's data
  • Wallet balance is shared at the user level (across all client_ids under the same user)

Resolution

If client_id is provided at initialization, the SDK uses it directly (no network calls). If omitted, the SDK calls GET /auth/whoami — if already bound, uses the existing client_id; if not, calls POST /auth/bootstrap to generate and bind a new one.

Best Practice

Persist the resolved client_id after the first initialization and pass it explicitly on subsequent sessions. This avoids unnecessary network calls and ensures identity continuity across API Key rotations.

Data Isolation

| Scope | Isolation Level | |-------|----------------| | Wallet balance | User-level — shared across all client_ids under the same user | | Cards | client_id-scoped — only visible to the client_id that created them | | Transactions | client_id-scoped — only visible to the client_id that owns the card | | Recharge addresses | User-level — shared |

API

// Wallet
const wallet = await client.walletDetail();
const addresses = await client.rechargeAddresses(wallet.wallet_id);

// X402 — chain_code defaults to "ETH" if omitted
const payee = await client.x402PayeeAddress({ token_code: "USDC" });
const asset = await client.x402AssetAddress({ token_code: "USDC", chain_code: "BASE" });

// Cards
const order = await client.newCard(params);
const { data } = await client.cardList({ page: 1, page_size: 20 });
const balance = await client.cardBalance("card-id");
const balances = await client.batchCardBalances(["card-id-1", "card-id-2"]);
const details = await client.cardDetails("card-id");
const updated = await client.updateCard("card-id", { client_request_id: "uuid", tx_limit: "200.0000" });

// Transactions
const txs = await client.transactionList({ card_id: "card-id", page: 1, page_size: 20 });

// Refill
const refill = await client.refillCard("card-id", params);

Batch Card Balances

Query multiple card balances in a single request:

const { data } = await client.batchCardBalances(["card-id-1", "card-id-2"]);
// data: CardBalanceResponse[] — same order as input card_ids

Update Card Controls

Update a card's spending controls (transaction limit, MCC filters):

const result = await client.updateCard("card-id", {
  client_request_id: crypto.randomUUID(),
  tx_limit: "200.0000",          // per-transaction limit
  allowed_mcc: "5411,5812",      // MCC whitelist (OR blocked_mcc, not both)
});
// result.status: "success" | "pending_external"

At least one update field must be provided. allowed_mcc and blocked_mcc are mutually exclusive — set one or the other, not both. The server creates an update order and calls the issuer. If the issuer responds asynchronously, status will be "pending_external" and the final result arrives via webhook.

Mode A — Wallet Funded Card

Mode A is the simplest path: cards are paid from your Clawallex wallet balance. No blockchain interaction needed.

Create a Card

import { ModeCode, CardType } from "@clawallex/sdk";

const order = await client.newCard({
  mode_code: ModeCode.WALLET,  // Mode A
  card_type: CardType.FLASH,   // FLASH (single-use) or STREAM (rechargeable)
  amount: "50.0000",           // card face value in USD
  client_request_id: crypto.randomUUID(),  // idempotency key
  // Optional spending controls:
  tx_limit: "100.0000",        // per-transaction limit
  allowed_mcc: "5411,5812",    // MCC whitelist (OR blocked_mcc, not both)
});

// order.card_order_id — always present
// order.card_id       — present if card created synchronously
// order.status        — 200=active, 120=pending_async (issuer processing)

Handling Async Card Creation (status=120)

Card creation may be asynchronous — the issuer accepts the request but hasn't finished yet. This is normal, not an error. The wallet has already been charged.

if (order.status === 120 || !order.card_id) {
  // Poll card list until the new card appears
  const before = await client.cardList({ page: 1, page_size: 100 });
  const existingIds = new Set(before.data.map(c => c.card_id));

  let cardId: string | undefined;
  for (let i = 0; i < 30; i++) {
    await new Promise(r => setTimeout(r, 2000));
    const list = await client.cardList({ page: 1, page_size: 100 });
    const newCard = list.data.find(c => !existingIds.has(c.card_id));
    if (newCard) {
      cardId = newCard.card_id;
      break;
    }
  }
}

Tip: You can also retry newCard with the same client_request_id. The server will safely retry the issuer call without re-charging your wallet.

Mode A Refill

const refill = await client.refillCard(cardId, {
  amount: "30.0000",
  client_request_id: crypto.randomUUID(),  // idempotency key for Mode A
});

Fee Structure

Fees are calculated server-side. For Mode B, the 402 response breaks them down:

| Fee field | Applies to | Description | |-----------|-----------|-------------| | issue_fee_amount | All cards | One-time card issuance fee | | monthly_fee_amount | Stream cards only | First month fee (included in initial charge) | | fx_fee_amount | All cards | Foreign exchange fee | | fee_amount | — | = issue_fee_amount + monthly_fee_amount + fx_fee_amount | | payable_amount | — | = amount + fee_amount (total to pay) |

  • Flash cards: fee_amount = issue_fee + fx_fee
  • Stream cards: fee_amount = issue_fee + monthly_fee + fx_fee
  • Mode A refill: no fees — the refill amount goes directly to the card
  • Mode B refill: no fees — same as Mode A

Mode B — x402 On-Chain Payment (Two-Step)

Mode B is for Agents that hold their own wallet and private key. The card is funded by an on-chain USDC transfer via the EIP-3009 transferWithAuthorization standard — no human intervention needed.

Mode B currently only supports USDC (6 decimals) on ETH and BASE chains. token_code must be "USDC".

Flow

Agent → POST /card-orders (mode_code=200)     → 402 + quote details
Agent → sign EIP-3009 with private key
Agent → POST /card-orders (same client_request_id) → 200 + card created

Stage 1 — Request Quote (402 is expected, not an error)

import {
  ClawallexPaymentRequiredError,
  ModeCode,
  CardType,
  type CardOrder402Details,
} from "@clawallex/sdk";

const clientRequestId = crypto.randomUUID();
let details: CardOrder402Details;

try {
  await client.newCard({
    mode_code: ModeCode.X402,
    card_type: CardType.STREAM,  // FLASH or STREAM
    amount: "200.0000",
    client_request_id: clientRequestId,
    chain_code: "ETH",           // or "BASE"
    token_code: "USDC",
  });
} catch (err) {
  if (err instanceof ClawallexPaymentRequiredError) {
    details = err.details;
    // details contains:
    //   payee_address    — system receiving address
    //   asset_address    — USDC contract address
    //   payable_amount   — total including fees (e.g. "207.5900")
    //   x402_reference_id — must be echoed in Stage 2
    //   final_card_amount, fee_amount, issue_fee_amount, monthly_fee_amount, fx_fee_amount
  }
}

EIP-3009 Signing (using ethers.js)

import { ethers } from "ethers";

const wallet = new ethers.Wallet(PRIVATE_KEY);
const maxAmountRequired = String(Math.round(parseFloat(details.payable_amount) * 1_000_000));
const now = Math.floor(Date.now() / 1000);
const nonce = ethers.hexlify(ethers.randomBytes(32));

const signature = await wallet.signTypedData(
  {
    name: "USDC",                        // query via contract.name() — varies by chain
    version: "2",
    chainId: 11155111,                   // Sepolia; ETH mainnet=1, BASE=8453
    verifyingContract: details.asset_address,
  },
  {
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" },
    ],
  },
  {
    from: wallet.address,
    to: details.payee_address,
    value: maxAmountRequired,
    validAfter: String(now - 60),
    validBefore: String(now + 3600),
    nonce,
  },
);

Note: The EIP-712 domain name depends on the USDC contract deployment. On Sepolia testnet it is "USDC", on mainnet it may be "USD Coin". Query the contract's name() method to confirm.

Stage 2 — Submit Payment

IMPORTANT: Stage 2 must use the same client_request_id as Stage 1. A different client_request_id will create a new card order instead of completing the current one.

The SDK provides typed interfaces X402PaymentPayload and X402PaymentRequirements for full autocomplete support:

import type { X402PaymentPayload, X402PaymentRequirements } from "@clawallex/sdk";

const order = await client.newCard({
  mode_code: ModeCode.X402,
  card_type: CardType.STREAM,
  amount: "200.0000",
  client_request_id: clientRequestId,    // MUST reuse from Stage 1
  x402_version: 1,
  payment_payload: {
    scheme: "exact",
    network: "ETH",
    payload: {
      signature,
      authorization: {
        from: wallet.address,
        to: details.payee_address,         // must equal payee_address
        value: maxAmountRequired,
        validAfter: String(now - 60),
        validBefore: String(now + 3600),
        nonce,
      },
    },
  },
  payment_requirements: {
    scheme: "exact",
    network: "ETH",                        // must equal payload.network
    asset: details.asset_address,          // must equal 402 asset_address
    payTo: details.payee_address,          // must equal authorization.to
    maxAmountRequired,                     // must equal authorization.value
    extra: {
      referenceId: details.x402_reference_id,
    },
  },
  extra: {
    card_amount: details.final_card_amount,  // must equal original amount
    paid_amount: details.payable_amount,     // must equal amount + fee
  },
  payer_address: wallet.address,
});
// order: { card_order_id, card_id, status }

Mode B Refill (No 402 — Direct Submit)

Refill has no 402 challenge. Query addresses first, then submit directly:

// 1. query addresses
const payee = await client.x402PayeeAddress({ token_code: "USDC", chain_code: "ETH" });
const asset = await client.x402AssetAddress({ token_code: "USDC", chain_code: "ETH" });

// 2. sign EIP-3009 (same as above, but amount has no fee)
const refillAmount = "30.0000";
const maxAmt = String(Math.round(parseFloat(refillAmount) * 1_000_000));
// ... sign with wallet ...

// 3. submit refill
const refill = await client.refillCard(cardId, {
  amount: refillAmount,
  x402_reference_id: crypto.randomUUID(),  // unique per refill
  x402_version: 1,
  payment_payload: { /* same structure as card-orders */ },
  payment_requirements: { /* same structure */ },
  payer_address: wallet.address,
});

Consistency Rules (Server Rejects if Any Fail)

| # | Rule | |---|------| | 1 | payment_payload.network == payment_requirements.network | | 2 | authorization.to == payTo == 402 payee_address | | 3 | authorization.value == maxAmountRequired == payable_amount × 10^6 | | 4 | payment_requirements.asset == 402 asset_address | | 5 | extra.referenceId == 402 x402_reference_id | | 6 | extra.card_amount == original amount | | 7 | extra.paid_amount == 402 payable_amount |

Card Details — Decrypting PAN/CVV

The cardDetails response includes card controls and cardholder info alongside encrypted PAN/CVV:

| Field | Description | |-------|-------------| | tx_limit | Per-transaction spending limit | | allowed_mcc | MCC whitelist (comma-separated; mutually exclusive with blocked_mcc) | | blocked_mcc | MCC blacklist (comma-separated; mutually exclusive with allowed_mcc) | | first_name | Cardholder first name | | last_name | Cardholder last name | | delivery_address | Billing address (JSON string or plain text) | | encrypted_sensitive_data | Encrypted PAN/CVV (see below) |

The server encrypts sensitive data with a key derived from your api_secret.

import { createHash, createHmac, createDecipheriv } from "node:crypto";
import { hkdf } from "node:crypto";

const details = await client.cardDetails("card-id");
const enc = details.encrypted_sensitive_data;
// enc.version = "v1", enc.algorithm = "AES-256-GCM", enc.kdf = "HKDF-SHA256"

// 1. Derive 32-byte key from api_secret using HKDF-SHA256
const ikm = Buffer.from(API_SECRET);
const salt = Buffer.alloc(0);
const info = Buffer.from("clawallex-card-sensitive-data");
const derivedKey = await new Promise<Buffer>((resolve, reject) => {
  hkdf("sha256", ikm, salt, info, 32, (err, key) => {
    err ? reject(err) : resolve(Buffer.from(key));
  });
});

// 2. Decrypt with AES-256-GCM
const nonce = Buffer.from(enc.nonce, "base64");
const ciphertext = Buffer.from(enc.ciphertext, "base64");
const authTag = ciphertext.subarray(ciphertext.length - 16);
const encrypted = ciphertext.subarray(0, ciphertext.length - 16);

const decipher = createDecipheriv("aes-256-gcm", derivedKey, nonce);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);

const { pan, cvv } = JSON.parse(decrypted.toString("utf-8"));
// pan = "4111111111111111", cvv = "123"

Security: Never log or persist the decrypted PAN/CVV in plaintext. The api_secret must be at least 16 bytes.

Error Handling

import { ClawallexApiError, ClawallexPaymentRequiredError } from "@clawallex/sdk";

try {
  await client.newCard(params);
} catch (err) {
  if (err instanceof ClawallexPaymentRequiredError) {
    // Mode B step 1 — normal flow, proceed with on-chain payment
    const { payee_address, asset_address, payable_amount } = err.details;
  } else if (err instanceof ClawallexApiError) {
    console.error(err.statusCode, err.code, err.message);
  }
}

Enums Reference

| Constant | Named Constant | Value | Description | |----------|---------------|-------|-------------| | mode_code | ModeCode.WALLET | 100 | Mode A — wallet funded | | mode_code | ModeCode.X402 | 200 | Mode B — x402 on-chain | | card_type | CardType.FLASH | 100 | Flash card | | card_type | CardType.STREAM | 200 | Stream card (subscription) | | card.status | 200 | Active | | card.status | 220 | Closing | | card.status | 230 | Expired | | card.status | 250 | Cancelled | | wallet.status | 100 | Normal | | wallet.status | 210 | Frozen |