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

@certen.io/sdk

v0.12.1

Published

TypeScript client for the CERTEN Gateway API — proof-gated cross-chain execution on Accumulate

Readme

@certen.io/sdk

TypeScript client for the CERTEN Gateway — proof-gated cross-chain execution on Accumulate.

Docs: https://docs.kompendium.co · Get an API key: https://gateway.kompendium.co/portal

npm install @certen.io/sdk

Requires Node 22 or 24 (engines.node >=22). To sign through the SDK, also install the optional peer @certen.io/proof-verify (see Every signature is checked first).

import { CertenClient } from '@certen.io/sdk';

const certen = new CertenClient({ apiKey: process.env.CERTEN_API_KEY! });

// One call: opens the intent, signs what comes back, submits the signature.
const { intentId } = await certen.execute.transfer({
  identityId,
  adiUrl: 'acc://your-org.acme',
  fromChain: 'accumulate',
  toChain: 'base-sepolia',
  fromAddress: abstractAccount,      // the identity's account on the destination chain
  toAddress: '0xBe00…9251',
  amount: '0.001',                   // WHOLE units, as a string
  sign: (hashHex) => ed25519Sign(hashHex),   // YOU hold the key
});

await certen.execute.wait(intentId);                 // ~60-110s of real validator work
const proof = await certen.execute.proof(intentId);  // hand this to your counterparty

execute.send is the same transfer with everything the identity already knows filled in: it derives the ADI and the abstract account from identityId, and refuses before the network is touched or anything is signed a recipient that is not an address (or whose EIP-55 checksum fails), an asset that is not the chain's native token, an amount that is zero or looks like wei, and a chain the catalogue does not know.

const { intentId, fromAddress } = await certen.execute.send({
  identityId, chain: 'base-sepolia', toAddress: '0xBe00…9251', amount: '0.001', sign: (hashHex) => ed25519Sign(hashHex), publicKey,
});

Prefer execute.* — it is the same flow every integration writes, with the details that are easy to get wrong already handled. The raw resources are there when you need to drive the steps yourself:

const opened = await certen.transaction.create({ identityId, intent });
// `signing_data` is absent in provider mode, where the gateway holds the key and there is
// nothing for you to sign.
const hash = opened.signing_data?.hash_to_sign;
if (!hash) throw new Error('provider mode: no signature required from you');
await certen.transaction.submitSignature(opened.intent_id, {
  signature: ed25519Sign(hash),
  publicKey,
});

Your key never leaves your process. The gateway computes what must be signed and you decide whether to sign it. That is the supported posture for anything carrying value — see the external-signing guide.


Configuration

new CertenClient({
  apiKey: 'ck_live_…',           // required
  baseUrl: 'https://…',           // optional — see precedence below
  maxRetries: 3,                  // default 3
  baseBackoffMs: 250,
  maxBackoffMs: 8_000,
  autoIdempotencyKey: true,       // default: every POST gets one
});

Base URL precedence: options.baseUrl → $CERTEN_API_URL → https://gateway.kompendium.co. The env var is there so a deployment can be retargeted at staging or a self-hosted gateway without a code change.

The client only talks to its own gateway. Every request URL, every submit_url a response names, and every redirect must stay on the base URL's origin; anything else is refused with FOREIGN_ORIGIN_URL before a credential is attached or anything is signed. Every id, hash or token you pass is encoded as exactly one path segment; an empty one, or one made only of dots, is refused with INVALID_PATH_PARAMETER instead of reaching a different route.

Browsers

@certen.io/sdk runs in a browser through the browser export condition (webpack, Vite, esbuild and rollup select it automatically), or explicitly as @certen.io/sdk/browser. That entry is the client, its resources, the typed errors, the status table, the chain catalogue and the pure helpers; it uses Web Crypto and no Node module. Two things need Node and are only in the default entry: CertenAgent / ed25519Signer (a local signing key) and fetchSharedProof / decodeSharedBundle (gunzip). A browser sends no User-Agent (it forbids scripts setting one). verifyReceipt uses Web Crypto's ed25519; a runtime without it reports the signature check as skipped, never as ok. CI bundles the browser entry and runs it with process, Buffer and require removed.

Every signature is checked first

execute.contractCall, execute.transfer, execute.cosign and CertenAgent's governance operations never sign a bare hash_to_sign. They rebuild the transaction the gateway returned (signing_data.transaction, signing_data.signature_metadata), recompute every hash and match what it would authorise to your request, then sign; any disagreement throws SIGNING_DATA_MISMATCH naming the field, and nothing is signed. beforeSign(summary) shows what the signature authorises and may decline. There is no option to sign without the check. It needs the optional peer @certen.io/proof-verify, and a gateway that returns the unsigned transaction; in a browser, signing through the SDK is refused until the verifier runs there.

Verify a proof: @certen.io/sdk/verify

A separate entry point, so the API client stays light: it needs the optional peer @certen.io/proof-verify (and, under it, the Accumulate encoder).

import { CertenClient } from '@certen.io/sdk';
import { loadProofEvidence, verifyBundle, bundleInputOf } from '@certen.io/sdk/verify';

const client = new CertenClient({ apiKey });
const evidence = await loadProofEvidence(client, intentId);          // bundle + proof v2 document (or why there is none)
const v = verifyBundle(bundleInputOf(evidence), { header });         // header: the execution chain's block header, from your own node
v.overall;  // 'verified' | 'partial' | 'failed' | 'no_evidence'
v.layers;   // one verdict per layer, with the evidence it was checked against

The Accumulate side is checked from the proof v2 portable document, the execution outcome from the bundle's own receipt. A verified field in a bundle is never read as a verdict (it comes back as bundleStatements), and neither is the gateway's own receipt. When the gateway serves no proof v2 document for a proof, proof.portable() throws PROOF_V2_EVIDENCE_NOT_SERVED and loadProofEvidence reports it as notServed: the result is no_evidence, never a pass.

The receipt

buildReceipt is the whole answer to "what happened": it reads the intent, verifies its proof here, optionally reads the recipient's balance from a node of yours, and assembles one record (assembleReceipt is the pure half, in the main entry, and needs no verifier).

import { buildReceipt } from '@certen.io/sdk/verify';

const receipt = await buildReceipt(client, intentId, { rpc: process.env.MY_NODE, request });
receipt.verdict;  // 'verified' | 'partial' | 'failed' | 'pending' | 'unproven'
receipt.gaps;     // what is not established, one entry each: empty exactly when verdict is 'verified'
receipt.ids;      // intent, Accumulate tx, destination tx (from the verified proof), proof id

verified needs the proof to verify and the value to have arrived. The proof's execution layer stays unchecked until the receipts root is compared with a header from a node you named, so without rpc a receipt is partial and says which layer. When the process must not call a node itself (a server an agent drives), fetch the block header from your own node and pass it as header: { receiptsRoot, hash?, number? } instead: it closes the same layer, cannot read the recipient (so a transfer stays partial), is refused with INVALID_HEADER if it has no receiptsRoot (a header with nothing to compare is not a comparison), and cannot be combined with rpc. @certen.io/mcp's certen_receipt is this call. A proof that could not be fetched is unavailable (not "no evidence"), a recipient that could not be read is unreadable, and neither is ever turned into a pass. rpc is yours: no CERTEN credential is sent to it, no redirect is followed, and its URL (which can carry a key) is never put in a message or the receipt, only its host.

What it does for you

Automatic idempotency on POSTs. Every POST gets a generated Idempotency-Key so a retried network error is safe to replay. This matters more than it sounds: without one, a timeout is indistinguishable from success, and a blind retry can open a second intent or burn identity quota. Opt out per-client with autoIdempotencyKey: false, or per-route with noAutoIdempotencyRoutes.

Retries with backoff, and rate-limit awareness. Transient failures retry with exponential backoff. On a 429 the client remembers the reset window and pre-emptively waits rather than hammering.

Typed errors. Catch the class you care about instead of matching on status codes:

import { CertenAuthError, CertenRateLimitError, CertenBadRequestError, CertenServerError } from '@certen.io/sdk';

try {
  await certen.identity.create({ … });
} catch (e) {
  if (e instanceof CertenBadRequestError) { /* your request — fix and retry */ }
  if (e instanceof CertenRateLimitError)  { /* back off; e carries the reset */ }
  if (e instanceof CertenAuthError)       { /* key is wrong, revoked, or lacks the scope */ }
  if (e instanceof CertenServerError)     { /* gateway or downstream — safe to retry */ }
}

Pagination helpers. paginate and paginateWithTotal iterate a paged endpoint as an async generator.

An agent, as one object

CertenAgent is the composition an autonomous agent needs: one identity, accounts on any chain, and every proof-gated verb, with the key held by a signer that never leaves the process.

import { CertenClient, CertenAgent, ed25519Signer } from '@certen.io/sdk';

const signer = ed25519Signer(process.env.AGENT_SEED);            // 64 hex; or ed25519Signer() and persist .seedHex
const agent = new CertenAgent(new CertenClient({ apiKey }), signer);

await agent.provision({ name: 'seller-bot', chains: ['base-sepolia'] });
await agent.linkChain('arbitrum-sepolia');                        // same ADI, no new identity, no new key

const intent = await agent.call({
  chain: 'base-sepolia',
  call: { target: escrobot, functionSignature: 'ship(bytes32,string)', args: [orderId, 'TRACK-1'],
          expectedEvents: [{ contract: escrobot, topic0: shippedTopic }] },
});
await agent.wait(intent.intentId);
const link = await agent.share(intent.intentId);                  // verifiable by a stranger, no account needed

await agent.token({ chain: 'base-sepolia', token: usdc, to, amount: '1000000' });   // 1 USDC, proof-gated
await agent.governance.requireSigner('acc://owner-policy.acme/book');               // the owner's rules gate every spend
await agent.governance.requireSigner('acc://owner-policy.acme/book', { account: 'book' }); // ...and who may act for the agent

Resume an existing agent by passing its saved state (identity id, ADI, accounts) as the third constructor argument. An escrow or any other contract ABI belongs on top of call(), not inside.

Resources

| | | |---|---| | certen.identity | create · get · update · retire · readiness · topUpCredits · drip | | certen.transaction | create · quote · submitSignature · get · list | | certen.chains | list · get · enabled · balance | | certen.sign | create · submitSignature | | certen.governance | create · submitSignature · get | | certen.pending | list | | certen.portfolio | get | | certen.admin | createOrg · createApiKey · listApiKeys · revokeApiKey · rotateApiKey · getAuditLog · getUsage |

The gateway serves ~80 endpoints; this covers the integration surface. For anything else, the live spec is authoritative and the client's request path will carry it.

Chains

One typed catalogue, CHAIN_CATALOGUE, holds every chain fact the SDK, the CLI and the MCP server use: slug, numeric id, gas token and decimals, block time, faucet, aliases, and whether it is on by default.

| Chain | Id | Gas | Default | |---|---|---|---| | ethereum-sepolia | 11155111 | ETH | on | | base-sepolia | 84532 | ETH | on | | arbitrum-sepolia | 421614 | ETH | on | | telcoin-adiri — Telcoin's Adiri testnet | 2017 | TEL (18 decimals) | off |

The catalogue says what a chain IS; the gateway says whether it may be used. Offer what client.chains.enabled(configured?) returns: the configured chains (default: the three above) that GET /v1/chains serves right now. parseEnabledChains('…,telcoin-adiri') turns a setting into that list and refuses an unknown or retired chain by name.

The funding guard reads each chain's own gas token (readNativeBalance). On Adiri an empty TEL account throws CertenUnfundedAccountError; a gas balance it cannot identify (an unknown chain, a balance row that is not the chain's gas token, or an unreadable one) throws CertenFundingUnverifiableError rather than letting the intent through.

Reads, estimates, and two calls that spend the platform's funds

// Reads (nothing is spent or created)
await certen.chains.balance('base-sepolia', '0x…');            // any address's native balance, as a decimal string: '0.25'; '0' is an empty account
await certen.identity.readiness(id, { chain: 'base-sepolia' }); // identity, key, credits, chain account + balance, org balance: one read, facts only
await certen.transaction.quote({ intent });                     // what it would cost and whether it would be admitted: an ESTIMATE, nothing created

// Calls that spend the PLATFORM's funds. Each needs an Idempotency-Key and is OFF unless the deployment's operator switched it on.
await certen.identity.topUpCredits(id, { credits: 500, idempotencyKey: 'order-4411' }); // sponsor-paid ACME credits (1 credit = $0.01)
await certen.identity.drip(id, 'base-sepolia', { idempotencyKey: 'drip-77' });          // testnet gas to the identity's own account

balance needs an API key (it reads through the gateway's node), and a node that did not answer is BALANCE_UNREADABLE, never a zero. readiness reports facts and says nothing like "ready"; each section has its own state, and an unreadable one does not fail the others. client.readiness() is the longer way to the same question: it checks the key page against Accumulate itself, which the gateway's aggregate does not.

topUpCredits and drip return 202 with status: 'unconfirmed' when nobody can say whether the money moved. That is data, not an error: read the balance before asking again, because a new key is a second draw. Pass your own idempotencyKey so a retry after a crash reuses the same one; left out, the SDK generates one for that call (and the same one on its own internal retries). Both are refused with a code you can read when switched off (CREDIT_TOPUP_DISABLED, DRIP_DISABLED; not retried), limited (CREDIT_TOPUP_LIMIT, DRIP_LIMIT), or, for the drip, aimed at something that is not a testnet (DRIP_TESTNET_ONLY). The drip sends nothing anywhere you name: the destination is the account the gateway recorded for the identity and the amount is the operator's.

Three things to know

Identity creation is asynchronous. identity.create resolves with a 202-shaped result and provisioning continues. Poll identity.get until the status is terminal, and check can_sign before building on it.

A proof cycle takes ~60–110 seconds. Real validator work, not a tunable delay. Do not wrap it in a 30s timeout.

vote is approve | reject | abstain — a lowercase string, not a number, and not accept.

Deadlines, header authorities and failure reasons

execute.contractCall, execute.transfer, transaction.create, agent.call and agent.transfer accept expiresAt (a Date or RFC 3339 string, or expiresIn('30m'); 90 s to 7 days ahead — the gateway allows 60 s, the SDK keeps a margin) and additionalAuthorities (acc:// key books, at most 8). Both are validated before anything is sent.

  • expiresAt — unsigned past the deadline, the intent ends failed with reason_code: 'expired': nothing executes, no fee or gas.
  • additionalAuthorities is refused by the gateway by default — HTTP 422, thrown as CertenHeaderAuthorityNotExecutableError (see its guidance). Validators do not yet count a header authority's signature, so such an intent would never execute. Make a required co-signer an authority on the account instead (agent.governance.requireSigner).
  • execute.wait() throws CertenIntentFailedError on a failed intent, with reasonCode ('expired', 'expectation_unmet', 'target_reverted', …). describeReasonCode() renders it. expectation_unmet means the call ran but its expectedEvents are missing: it is not a success.

Guides

License

MIT