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

aicard-sdk

v0.1.0

Published

AiCard Custos SDK — agent onboarding, x402 USDC recharge, LLM gateway access

Readme

aicard-sdk

Tiny SDK for the AiCard Custos platform. Two layers, pick one:

v3.1 — Virtual Cards (browser + wallet)

issueCardWithSigner → openCard → card.chat()           ← the agent runtime
loginAsOwner → listMyCards / revokeCardWithSigner /
               renewCardWithSigner / rechargeWithSigner ← the owner UI

Owner connects a normal Web3 wallet (MetaMask, WalletConnect, etc.) and signs a policy commitment — Custos persists the card, enforces server-side limits (perTx/perDay/perWeek/total USDC, model whitelist, velocity, Credo gating), and forwards chat/payment to Mall. No CDP smart account required.

v2 — Agent SDK (server-side, viem private key)

register → exchangeToken → activateMall → recharge (x402) → chat

Lets any agent runtime — AWS AgentCore, Bedrock (via Lambda), Cursor / Hermes (via MCP), OpenClaw, or your own Node process — onboard onto Mall and top up USDC over x402 with a viem-compatible private key.

Test-env first. Both layers hard-lock to Base Sepolia (chainId: 84532) for on-chain settlement. Mainnet routes ship separately with stricter limits.

Install

npm i aicard-sdk viem
# or
pnpm add aicard-sdk viem

In this monorepo it's a workspace package:

// package.json
"dependencies": { "aicard-sdk": "workspace:*" }

v3.1 — Virtual Card flow

Issue a card (owner side, browser)

Owner signs once with their connected wallet. No CDP smart account is created; the wallet itself is the on-chain identity.

import { CustosSDK } from "aicard-sdk"
import { useAccount, useSignMessage } from "wagmi"

const { address }          = useAccount()
const { signMessageAsync } = useSignMessage()
const sdk = new CustosSDK({ baseUrl: "https://sepolia.aicard.credit" })

const result = await sdk.issueCardWithSigner(
  {
    address,
    signMessage: ({ message }) => signMessageAsync({ message }),
  },
  {
    label:      "demo-card",
    limits:     { perTxUsdc: 0.5, perDayUsdc: 2, perWeekUsdc: 10, totalUsdc: 50 },
    policy: {
      allowModels:   ["anthropic/*"],
      velocity:      { perMin: 30, perHour: 600 },
      minCredoScore: null,             // "A" | "B" | "C" | null
    },
    validUntil: "2026-12-31T23:59:59.000Z",
  },
)
//  ↳ { cardId, agentApiKey, agentSecretKey, policyHash, provisioning, ... }
//
// agentSecretKey is shown ONLY here. Persist it immediately — the server
// only keeps a hash.
//
// provisioning.mall === "provisioned" means a fresh Mall account was set up
// for this wallet automatically; "inherited" means an existing one was reused.

Use the card (agent runtime, server-side)

The card's three credentials (cardId / agentApiKey / agentSecretKey) get handed to whatever agent runtime needs to spend. Open the card to mint a 1-hour bearer, then route chat through the policy-gated proxy.

import { CustosSDK } from "aicard-sdk"
const sdk = new CustosSDK({ baseUrl: "https://sepolia.aicard.credit" })

const card = await sdk.openCard({ cardId, agentApiKey, agentSecretKey })

const reply = await card.chat({
  model:    "anthropic/claude-haiku-4-5-20251001",
  messages: [{ role: "user", content: "Hello." }],
})

const usage = await card.usage()    // current per-day/week/total spend
const policy = await card.policy()  // policy snapshot for diagnostics

The proxy enforces every policy rule before forwarding to Mall:

| Gate | Failure code | |--------------------------|------------------------------| | Card is ACTIVE | card_revoked / card_frozen | | validUntil not passed | card_expired | | Mall provisioning ready | mall_not_provisioned (503) | | Model in allowModels | model_not_allowed | | credoScoreCached ≥ min | credo_below_threshold | | perMin / perHour | over_velocity_per_min/hour |

Manage cards (owner side, browser)

Owner mints a 5-minute bearer once, then lists / revokes / renews / recharges without re-signing for every call (only the action-bound steps re-sign).

// 1. One signature → 5-min owner bearer
const session = await sdk.loginAsOwner({
  address,
  signMessage: ({ message }) => signMessageAsync({ message }),
})

// 2. List your cards (paginated, no re-signing)
const page1 = await sdk.listMyCards(session, { limit: 10 })
const page2 = await sdk.listMyCards(session, { cursor: page1.nextCursor })

// 3. Revoke — one wallet signature
await sdk.revokeCardWithSigner(
  { address, signMessage: ({ message }) => signMessageAsync({ message }) },
  cardId,
)

// 4. Renew (extend validUntil; limits + policy unchanged) — one signature
await sdk.renewCardWithSigner(
  { address, signMessage: ({ message }) => signMessageAsync({ message }) },
  cardId,
  "2027-01-31T23:59:59.000Z",
)

Top up Mall balance (web3 wallet → USDC on Base Sepolia)

The connected wallet signs EIP-3009 transferWithAuthorization; the facilitator settles on-chain. No gas paid by the user (only the typed-data signature). Make sure the wallet's chain is Base Sepolia (84532) before signing — wagmi will reject typed-data with a chain-mismatch otherwise.

import { useSignTypedData } from "wagmi"
const { signTypedDataAsync } = useSignTypedData()

const r = await sdk.rechargeWithSigner(
  session,
  { address, signTypedData: (data) => signTypedDataAsync(data) },
  0.5,  // USDC
)
//  ↳ { amountUsdc, balanceAdded, payTxHash, balance }

Wallet must hold ≥ amount × 1.05 USDC on Base Sepolia (the Mall fee margin). Testnet faucet: portal.cdp.coinbase.com/products/faucet.


v2 — Agent SDK (server-side private key)

Use this when you control the agent's private key directly (CI, server-side runtime, no browser wallet).

import { CustosSDK } from "aicard-sdk"
import { generatePrivateKey } from "viem/accounts"

const sdk = new CustosSDK({
  baseUrl:     process.env.CUSTOS_BASE_URL,        // e.g. https://api.aicard.credit
  mallBaseUrl: process.env.SMARTCORE_BASE_URL,     // e.g. https://api.test.credo.aicard.credit
})

// 1. Generate (or load) the agent's private key. Keep it in-memory only.
const privateKey = generatePrivateKey()

// 2. Register — secretKey is returned ONLY here. Persist immediately.
const creds = await sdk.register(privateKey)
//   ↳ { agentId, apiKey, secretKey, smartAccountAddress, eoaAddress }

// 3. Exchange long-lived apiKey + secretKey for a short-lived Bearer token.
const token = await sdk.exchangeToken({ apiKey: creds.apiKey, secretKey: creds.secretKey })

// 4. Activate on Mall — provisions a mallApiKey + llmServiceKey + welcome bonus.
const mall  = await sdk.activateMall(token)

// 5. (Testnet only) Faucet 1 USDC to the agent EOA, then top up via x402.
await sdk.faucet(creds.eoaAddress, "usdc")
//   ...wait ~5s for the USDC transfer to confirm on Base Sepolia...
const r = await sdk.recharge(token, privateKey, 0.5)
//   ↳ { ok, amountUsdc, balanceAdded, payTxHash, balance }

// 6. Use the issued llmServiceKey to talk to any Mall-supported model.
const out = await sdk.chat(mall.llmServiceKey!, {
  model:    "anthropic/claude-haiku-4-5",
  messages: [{ role: "user", content: "Reply in exactly 5 words." }],
})
console.log(out.text)

Using Mall as a drop-in LLM provider

After activateMall you get the two values every LLM client needs:

| value | where to read it | what it is | |------------|---------------------------------|--------------------------------------------| | apiKey | mall.llmServiceKey | Bearer token for the LLM gateway | | baseURL | the SDK's mallBaseUrl option | OpenAI- and Anthropic-compatible endpoint |

Default baseURL:

https://api.test.credo.aicard.credit       ← Base Sepolia test env

The gateway exposes both OpenAI-style (POST /v1/chat/completions, GET /v1/models) and Anthropic-style (POST /v1/messages) paths, so you can plug it into any client that lets you override the base URL.

Drop-in: OpenAI SDK

import OpenAI from "openai"

const openai = new OpenAI({
  apiKey:  mall.llmServiceKey!,
  baseURL: "https://api.test.credo.aicard.credit/v1",
})

const r = await openai.chat.completions.create({
  model:    "anthropic/claude-haiku-4-5",
  messages: [{ role: "user", content: "Hello in 5 words." }],
})
console.log(r.choices[0].message.content)

Drop-in: Anthropic SDK

import Anthropic from "@anthropic-ai/sdk"

const anthropic = new Anthropic({
  apiKey:  mall.llmServiceKey!,
  baseURL: "https://api.test.credo.aicard.credit",  // no /v1 suffix — the SDK adds it
})

const r = await anthropic.messages.create({
  model:      "anthropic/claude-haiku-4-5",
  max_tokens: 256,
  messages:   [{ role: "user", content: "Hello in 5 words." }],
})
console.log(r.content[0].type === "text" ? r.content[0].text : "")

Drop-in: LangChain (ChatOpenAI)

import { ChatOpenAI } from "@langchain/openai"

const llm = new ChatOpenAI({
  apiKey:    mall.llmServiceKey!,
  modelName: "anthropic/claude-haiku-4-5",
  configuration: { baseURL: "https://api.test.credo.aicard.credit/v1" },
})

Drop-in: Vercel AI SDK

import { createOpenAI } from "@ai-sdk/openai"
import { generateText } from "ai"

const mallProvider = createOpenAI({
  apiKey:  mall.llmServiceKey!,
  baseURL: "https://api.test.credo.aicard.credit/v1",
})

const { text } = await generateText({
  model:  mallProvider("anthropic/claude-haiku-4-5"),
  prompt: "Hello in 5 words.",
})

Raw HTTP

curl https://api.test.credo.aicard.credit/v1/chat/completions \
  -H "Authorization: Bearer $MALL_LLM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-haiku-4-5","messages":[{"role":"user","content":"Hi"}]}'

Supported models

List them at runtime via GET /v1/models with the same Bearer token, or use any of the OpenRouter-style aliases the gateway routes upstream — e.g. anthropic/claude-haiku-4-5, openai/gpt-4o-mini, google/gemini-2.5-flash.

Provider-specific parameters

Common params (model, messages, max_tokens, temperature, top_p, stop, stream, tools / tool_choice, response_format) work through the OpenAI surface regardless of upstream — the gateway translates.

Anthropic-specific params (top_k, system as a top-level field, prompt caching headers, computer-use beta, metadata.user_id) aren't in OpenAI's typed schema. Two ways to reach them:

1. Pass-through via OpenAI's extra_body — quickest, no extra SDK:

await openai.chat.completions.create({
  model:    "anthropic/claude-haiku-4-5",
  messages: [{ role: "user", content: "Hi" }],
  // @ts-expect-error — non-standard OpenAI field, gateway forwards it
  top_k:    40,
})

2. Use the Anthropic SDK against the same baseURL — recommended for Anthropic-native workflows (caching, tool use, computer use):

import Anthropic from "@anthropic-ai/sdk"

const anthropic = new Anthropic({
  apiKey:  mall.llmServiceKey!,
  baseURL: "https://api.test.credo.aicard.credit",  // SDK appends /v1/messages
})

await anthropic.messages.create({
  model:      "anthropic/claude-haiku-4-5",
  max_tokens: 256,
  system:     "You are concise.",
  top_k:      40,
  messages:   [{ role: "user", content: "Hi" }],
})

Both paths share the same llmServiceKey and the same Mall balance — the gateway exposes /v1/chat/completions and /v1/messages side by side, so you can pick the SDK that matches the params you need.

The Bearer key only debits the Mall balance you topped up in step 5. When the balance runs out the gateway returns 402 Payment Required — call sdk.recharge(...) again (or sdk.ensureBalance(...)) to top up.

API reference

new CustosSDK(opts?)

| option | default | what it is | |---------------|------------------------------------------|---------------------------------------------------------------------| | baseUrl | http://localhost:3000 | Custos backend (this Next.js app) | | mallBaseUrl | https://api.test.credo.aicard.credit | SmartCore / Mall — only used by direct chat() | | fetch | globalThis.fetch | Injection point for non-Node runtimes (Edge, undici, etc.) |

v3.1 Card surface

| Method | Signs in wallet? | Purpose | |-------------------------------------------------------|------------------|---------------------------------------------------------------| | issueCardWithSigner(signer, spec) | ✓ once | Owner signs policy commitment, server persists card | | openCard({cardId, agentApiKey, agentSecretKey}) | — | Agent HMAC-exchanges for 1h card-mode bearer | | card.chat({model, messages, ...}) | — | Routes through enforcement proxy, debits Mall balance | | card.policy() / card.usage() | — | Read snapshot | | loginAsOwner(signer) | ✓ once | Owner gets 5-min bearer for list/recharge calls | | listMyCards(session, {cursor, limit}) | — | Paginated owner-scoped card list | | revokeCardWithSigner(signer, cardId) | ✓ once | Flip card to REVOKED, invalidate outstanding bearers | | renewCardWithSigner(signer, cardId, validUntil) | ✓ once | Extend expiry, recompute policyHash | | rechargeWithSigner(session, signer, amount) | ✓ once (EIP-712) | Wallet-signed x402 USDC recharge of Mall balance |

signer shapes:

// For message-signing actions (issue / login / revoke / renew)
type CardOwnerSigner = {
  address:     `0x${string}`
  signMessage: (args: { message: string }) => Promise<`0x${string}`>
}

// For typed-data x402 recharge
type X402Signer = {
  address:       `0x${string}`
  signTypedData: (data: unknown) => Promise<`0x${string}`>
}

Both shapes are satisfied by:

  • viem LocalAccount: { address, signMessage, signTypedData }
  • wagmi hooks: ({ message }) => useSignMessage().signMessageAsync({ message })

v2 Agent surface

register(privateKey: Hex) → AgentCredentials

SIWE flow:

  1. GET /api/v1/auth/challenge?address=…{ nonce, message }
  2. viem account.signMessage({ message }) (local key signs)
  3. POST /api/v1/auth/register with { address, signature, nonce }

Throws if secretKey is missing in the response — that means the EOA is already registered. secretKey is returned ONCE. If you lose it, register a new key.

exchangeToken({ apiKey, secretKey }) → accessToken

HMAC-SHA256 over v1:${unixTime}:${apiKey} using secretKey. The secret never leaves the agent. The returned Bearer token is short-lived (1h).

activateMall(accessToken) → MallActivation

Provisions a Mall account and an LLM gateway key. Idempotent — re-calling returns the same mallApiKey / llmServiceKey. The welcome bonus is credited once (first call only).

recharge(accessToken, privateKey, amountUsdc) → RechargeResult

Two-phase x402 flow:

  • Phase 1 — Custos opens an invoice with Mall and returns the accept block (asset, amount-with-fee, payTo, network).
  • Phase 2 — SDK signs an EIP-3009 TransferWithAuthorization locally with the agent's private key, POSTs the base64 payment header back to Custos, which forwards to Mall for on-chain settlement via the Coinbase facilitator.

Requirements:

  • Agent EOA must hold ≥ amount × 1.005 USDC on Base Sepolia (Mall takes a 0.5% relay fee). Use sdk.faucet(eoaAddress) for testnet or transfer mainnet USDC to creds.eoaAddress for prod.
  • chainId must be 84532 — the route hard-rejects otherwise.

faucet(eoaAddress, token?) → { ok, txHashes }

Drops ~1 USDC (or ETH) from the CDP testnet faucet into the agent's EOA. Useful for one-shot CI runs; rate-limited by CDP per address.

chat(llmServiceKey, { model, messages, maxTokens? }) → { text, usage, raw }

Direct call into the Mall LLM gateway. Tries the path that matches the model family (/v1/messages for Claude, /v1/chat/completions otherwise) and auto-falls back to the other on a non-200. usage is whatever the gateway returned — usually { input_tokens, output_tokens } (Anthropic) or { prompt_tokens, completion_tokens } (OpenAI).

Security model

  • Private keys never leave the agent process. v2 signs SIWE + EIP-3009 locally with viem. v3.1 only uses the connected wallet for signatures — agentSecretKey is generated server-side, returned once, then hashed.
  • secretKey / agentSecretKey is shown once. Subsequent registrations for the same EOA / re-issued cards intentionally don't return it. The SDK detects this and throws.
  • Test-env enforced server-side. Recharge endpoints (v2 and v3.1) reject any chainId !== 84532 with a 403. The v3.1 web3 recharge UI surfaces a chain-switch button if the wallet is on the wrong network.
  • Policy commitment is signed by the owner. Every card stores a policyHash derived from {limits, policy, validUntil} — server can prove the rules being enforced match what the owner signed at issue time.

End-to-end test

A reference flow for the v2 surface lives at scripts/test-sdk-register.ts:

pnpm dev                                      # terminal 1
npx tsx scripts/test-sdk-register.ts          # terminal 2

For v3.1, issue a card from /agent in any local dev server, then run the zero-dependency Node smoke test (no SDK required):

# Download from the issued-cards table on /agent — pre-filled with cardId
# and agentApiKey. Paste your agentSecretKey, then:
node ~/Downloads/test-card-xxxxxxxx.mjs

Expected:

[1/4] openCard …  bearer acquired (1h)
[2/4] card.policy() …
[3/4] card.usage() …
[4/4] card.chat() …  text:  Hello.