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

getcofund

v0.7.0

Published

Cofund API client, local signers and the `cofund` CLI. Keys are generated and used locally; Cofund never sees them.

Readme

getcofund

Client, local signers and the cofund CLI for the Cofund API (/api/v1).

Two kinds of key:

  • The API key is access. Cofund issues it.
  • The signing key is authority. It is generated on your machine and never leaves it. The API receives only addresses, xpubs and signatures.

Getting started

Below, cofund is npx getcofund, or the cofund that npm i -g getcofund installs.

1. What you need

  • An API key for your agent, issued by a Cofund platform admin in /internal, with the scopes it needs: read, propose, sign. An app acting for many people of its own asks for an app key instead (below).
  • COFUND_API_URL: the bare origin of the deployment, such as https://beta.getcofund.com, without /api/v1. COFUND_API_KEY: the key.
  • The CLI reads the process environment only, never a .env file. Export them first: set -a; source .env; set +a.

2. First run

cofund keys init
cofund me

keys init makes your EVM key and Bitcoin seed in ~/.cofund/keystore.json (mode 0600). They never leave this machine; never share the keystore. me prints your user id and scopes, then one line per cofund: company "acme-corp" is the identifier every signature carries, pinned in your keystore when you join or found the cofund; shown as "Acme Corp" is the name members see; then your role and the chains you hold keys on.

With an app key, create each person's user first and act as them; every step below then runs as that user:

cofund users create --external-id <your id for the person> --name "Nia Okafor"   # prints the user's id
export COFUND_USER=<that id> COFUND_KEYSTORE=~/.cofund/<that id>.json            # a keystore per user
cofund keys init
cofund me

In code: const user = await api.createUser({ externalId, name }), then new CofundClient({ baseUrl, apiKey, actAs: user.id }). See Managed users below.

3. Join or found a cofund

  • Invited: cofund invite accept shows the invite and asks you to pin its company. Without a terminal, pass --company <company> as whoever invited you gave it. It seats your address on each chain and sends your Bitcoin xpub.
  • Founding: cofund cofunds create --name "Acme Corp" --yes founds it with you as its admin and pins its company, which carries the deployment's namespace where it has one. cofund invite create --email <email> --first-name <name> --last-name <name> --position <title> [--admin] --yes invites each person and prints their user id and the invite id. Once they are in, cofund cofunds complete-setup --yes closes the onboarding phase on each chain. A chain shown closing is still landing: run it again until it shows closed.

4. People and vaults

  • cofund members: each member's user id, name <email>, role, and the chains they hold keys on.
  • cofund deposits: payments into the cofund's vaults, newest first, each with its sender (from, unknown when the chain does not tell), amount, pending or confirmed, and txid. --limit <n> (at most 100) and --before <id> page through them.
  • cofund vaults create --members <email|id,...> --threshold 2 (admins): the cofund's Bitcoin vault. The CLI rebuilds its address from its definition before printing it, then pins it. Fund only an address printed verified.
  • cofund vaults: each vault with its deployment's status (SUCCESS once on chain), a Bitcoin vault marked verified, unverified or MISMATCH. cofund vaults show <id>: its balances in whole tokens and base units.

5. Moving money

  • cofund policies create --vault <id> --name Payouts --signers <email|id,...> --threshold 2 --token <address> --max-amount 1000 [--recipients <address,...>] --yes (admins): at most 1,000 tokens per transfer, to the recipients listed, or to any recipient without --recipients.
  • cofund transfers propose --policy <id> --to <address> --amount 1.5 [--email <receipt>]: shows the transfer and asks; pass --yes where there is no terminal. Without --email no receipt is sent.
  • cofund requests: what awaits your signature. cofund requests show <id>: the request decoded here, and each check sign runs, as passed, FAILED or unverified. cofund requests sign <id> signs only what passes; cofund requests reject <id> (admins) withdraws it.

Amounts are whole tokens everywhere (1.5 is 1.5 USDC), and every preview shows them in base units too.

6. Safety

  • The chain domains (chain id and State) and the well-known tokens are pinned in the SDK; the CLI signs under its pins, never a server's. COFUND_CHAINS replaces the chain pins for local development: every command warns of it on stderr, and a mainnet chain id with another State is refused.
  • A cofund's company is pinned when you join or found it. Every signature carries it, and a request for another company is refused.
  • --allow-wrapper <address> vouches for a contract the SDK cannot decode. Use it only for a contract whose calls you can read yourself.
  • Taken on the server's word: which on-chain id or xpub belongs to which named member, an invite's callerId, where a Bitcoin vault's Cofund fee goes (shown before you sign), and the decimals of a token the SDK does not pin (marked "token not pinned").

7. Common errors

  • … is not an API endpoint on this host: COFUND_API_URL is wrong (the bare origin, without /api/v1), or the deployment does not have that endpoint yet.
  • Nothing was sent: no terminal to confirm in. Pass --yes …: read the preview, then run the command again with --yes.
  • Not vault-ready: <email> has no Bitcoin key in this cofund yet: that member has to accept their invite with a Bitcoin key first; this CLI sends one at invite accept.
  • 410: This request expired at …: its deadline passed and it takes no more signatures; propose it again.
  • 429: This key founded … cofunds in the last 24 hours …: the deployment's founding limit; the message says when the next is allowed.

The full docs: <COFUND_API_URL>/developers. The HTTP flow and every shape, for agents: <COFUND_API_URL>/api/v1/llms.txt and <COFUND_API_URL>/api/v1/openapi.json.

CLI

Run it with npx getcofund, or install it globally (npm i -g getcofund) and run cofund. The cofund package on npm is unrelated, so never use npx cofund.

npx getcofund keys init
# or
npm i -g getcofund
cofund keys init

With npx, put -- first when passing flags, or npm takes flags such as --help and --force for itself: npx -- getcofund invite accept --company "Acme".

cofund keys init [--force]
cofund me
cofund users create --external-id <id> --name <name>
cofund members
cofund cofunds create --name <name> [--company <identifier>] [--chains A,B] [--id <uuid>] [--yes]
cofund cofunds complete-setup [--close-without-stalled] [--yes]
cofund invite accept [--invite <id>] [--company <name>]
cofund invite create --email <email> --first-name <name> --last-name <name> --position <title> [--admin] [--chains A,B] [--yes]
cofund policies create --vault <id> --name <name> --signers <id|email,...> --threshold <n>
  (--token <address|BTC> --max-amount <amount> [--recipients <address,...>]
   | --wrapper <address> [--wrapper-name <name>]) [--type <PolicyType>] [--yes]
cofund requests [list]
cofund requests show <id> [--allow-wrapper <address>]...
cofund requests sign <id> [--allow-wrapper <address>]... [--rpc <url>] [--yes]
cofund requests reject <id> [--yes]
cofund transfers propose --policy <id> --to <address> --amount <amount> [--email <receipt>] [--name <name>] [--yes]
cofund deposits [--limit <n>] [--before <id>]
cofund vaults [list]
cofund vaults show <id>
cofund vaults create --members <id|email,...> --threshold <k> [--type ALL|ADMINS_INCLUDED|ADMINS_REQUIRED]
cofund stocks buy <SYMBOL> --spend <USDG> [--policy <id>] [--yes]
cofund stocks sell <SYMBOL> --shares <amount> [--policy <id>] [--yes]
cofund webhooks [list]
cofund webhooks add <url> [--events a,b]
cofund webhooks remove <id>

Commands on a cofund take --cofund <id> when the key is a member of several.

Amounts (--amount, --max-amount, --spend, --shares) are whole tokens: 1.5 is 1.5 USDC. They are converted with the token's pinned decimals, or for a token the SDK does not pin with the decimals Cofund states, which the preview marks "token not pinned"; a token neither knows is refused. Every preview prints the amount in whole tokens and in base units.

Environment:

  • COFUND_API_URL, COFUND_API_KEY: the API and your key.
  • COFUND_USER: with an app key, the managed user every command acts as (below).
  • COFUND_KEYSTORE: the keystore file. Default ~/.cofund/keystore.json, mode 0600.
  • COFUND_ROBINHOOD_RPC_URL: the Robinhood Chain RPC stocks buy and stocks sell quote from. Default the public one.
  • COFUND_<CHAIN>_SIMULATION_RPC_URL (COFUND_ETHEREUM_SIMULATION_RPC_URL, COFUND_ROBINHOOD_SIMULATION_RPC_URL): your own node for that chain, which requests sign re-runs a call's simulation on; --rpc <url> names it for one command.

Cofund runs on mainnet only. For local development, COFUND_CHAINS pins other chain ids and State addresses (JSON): every command then warns on stderr that the mainnet domains are replaced, and an entry that gives a mainnet chain id (1, 4663, 8453) another State is refused. COFUND_BTC_NETWORK=testnet makes testnet BTC keys.

cofunds create founds a cofund with you as its founding admin. It signs your own admin seat (InviteUser) on each pinned EVM chain (or --chains), 29 days out, in the company the deployment stores for the name: lowercase letters and digits joined by hyphens, behind the deployment's namespace when cofund me's API reports one ("Acme Corp" is acme-corp, or v2-acme-corp under the namespace v2); --company gives another. It sends your BTC xpub with its ownership proof, and pins the company. It prints the cofund's id and pins it before sending; a run cut short is retried with --id <that id>, which returns the cofund as it stands and deploys any vault the first run could not. It shows what it will sign and asks, or takes --yes. It prints each vault and the status of the transaction deploying it. The cofund starts in its onboarding phase, so your signature alone seats a co-admin (invite create --admin) and activates a policy. A Bitcoin vault comes later, with vaults create.

cofunds complete-setup finishes the setup of a cofund you founded, once its people are in: it signs CompleteOnboarding on each of its EVM chains under the pinned company, and the server relays them and marks setup done. From then on admin invites and policies take the admins' standing consensus. Admin seats you signed alone that are still joining hold it up, and the refusal names them; --close-without-stalled closes past seats whose joining stalled, which then have to be invited again. A chain whose lock is still landing prints closing, and the command says to run it again: running it again is harmless (closed or closing chains are not relayed twice) and prints closed once the lock landed.

invite accept checks the invite before signing anything:

  • it shows and pins the company;
  • the seat is yours;
  • it expires within 30 days.

It rebuilds the EIP-712 message itself under the pinned domain, signs locally, and sends the BTC xpub with its ownership proof.

requests lists what awaits you, as the app's Requests page shows it: transfers (EVM and Bitcoin), calls and, for an admin, policy activations and admin invites (one per chain). With nothing pending it says "Nothing awaits you in this cofund."

requests show runs the checks requests sign runs, without signing, and prints each as passed, FAILED or unverified: the domain, the company, the request decoded, the message rebuilt from its fields, the token against its pin; on a Bitcoin transfer the vault rebuilt from its definition and, once locked, the spend. A check it cannot run (no keystore, no company pinned, a spend not locked yet, a token not pinned) is unverified. A sponsored send's ERC-4337 UserOperation hash prints as userOp, apart from the transaction hash (tx), which appears once its bundle lands.

requests sign signs nothing it has not read:

  • the domain is the pinned one, and company is the one pinned at invite accept;
  • the message is rebuilt from the request's fields (a call's instructionsHash is hashed here), and must match the server's;
  • a call to the StockSwap wrapper goes through its decoder: the instructions must re-encode from the decoded swap, and the allowances must be exactly its input;
  • a call to any other wrapper is refused unread, unless you vouch for that wrapper with --allow-wrapper <address> (repeatable), for example your own contract; it then shows the raw instructions hash and allowances, and a wrapper Cofund does not list is flagged as unlisted;
  • a policy activation is rebuilt from its name, type, signers (shown as name <email>), threshold and bounds (token, cap, recipient allowlist or "any", or wrapper); one on an unlisted contract is shown as such and approved only with --allow-wrapper <address> for it; a BTC one is signed with the BTC key's Cofund leaf;
  • a call shows Cofund's simulation of it running now: the block, success or why it would revert, and one line per balance it changes, the vault first (Vault -3.0 USDC (0x…)), then the caveat and the most it can take (its allowances). With your own node (--rpc <url> or COFUND_<CHAIN>_SIMULATION_RPC_URL) it is simulated there too, from your address, and both are shown, the bound in base units; signing is refused when your node cannot simulate it, or when Cofund's simulation disagrees with yours beyond a block's difference (another outcome, or an amount more than 1% and more than dust off, other holders or assets included);
  • a token the SDK pins (USDC, USDT and WETH on Ethereum; USDG, WETH and the stock tokens on Robinhood; BTC) must be stated with its pinned symbol and decimals;
  • an admin invite must seat an admin, and its authorization must be open and at most 30 days out;
  • a Bitcoin transfer's vault is verified first: its definition (template, threshold, members' xpubs) is rebuilt with the app's own builder and must derive the vault's address, and your leaves come from that tree. The transfer is locked (its UTXOs; again, the same ones) and the spend checked: the first output pays the request's recipient its amount, then at most a Cofund fee within 1% of the amount and change back to the vault; inputs less outputs is the transfer's network fee and at most 1000 sat/vB. It signs one BIP-340 partial per input on each of your leaves (normally one), with the transaction rebuilt here as the vault's, so an input that is not the vault's or a false amount yields signatures no node accepts;
  • it shows the decoded request and asks, or takes --yes.

What it takes from the server: which on-chain user id (EVM) or account xpub (BTC) belongs to which named policy signer, an invite's callerId and authId (a wrong one only makes the chain refuse it), and which cofund a Bitcoin vault belongs to and where its Cofund fee goes (shown before you sign, with the vault's members and threshold). approve receives each signer with its id or xpub, so a script holding its own roster can check the pairing.

transfers propose proposes a transfer on a policy that names you. It shows the policy, the amount in whole tokens and base units, the recipient, the receipt email (--email; without it no receipt is sent and no contact is made) and the chain, and asks, or takes --yes. On EVM it signs the Transfer with your key, your first approval. On Bitcoin nothing is signed at proposal: it locks the UTXOs, checks the spend against what you asked for and signs your partials. The other signers approve in the app or with requests sign; at the threshold the server sends it.

vaults create (admins) makes the cofund's Bitcoin vault over the members named by user id or email, each with a Bitcoin key in it (an agent's is the xpub it accepted its invite with), at a threshold of 2 or more. A member must be in the vault to sign its transfers. Before printing the address it loads the vault's definition and rebuilds the address with the app's builder, under your keystore's network; then it pins the vault (its address and the hash of its definition) in the keystore. A vault that does not rebuild is refused loudly and its address is not given as one to fund. Signing a transfer from a pinned vault refuses one whose definition changed.

vaults lists each vault with the status of its deployment, a Bitcoin vault marked verified (its definition rebuilds its address, and matches the pin if any), unverified (no definition, a vault from before members had xpubs) or MISMATCH. vaults show <id> adds its balances in whole tokens and base units, whether they are stale (the chain did not answer and these are the last known), and a Bitcoin vault's template, threshold and members.

An admin creates policies and invites with keys of their own:

  • policies create builds the activation from the flags: the signers by member id or email (an EVM activation names their onchainId, a BTC one their account xpubs), the threshold, and the token, the cap per transfer in whole tokens and the recipient allowlist (--recipients; without it, any recipient), or the wrapper (function entry). It looks the wrapper up first (protocols(): the listed ones and your cofund's own unlisted ones): one new to the cofund, such as your own contract, needs --wrapper-name (not a listed protocol's name), and is registered by the server as your cofund's unlisted contract; an unlisted one is shown as such before you sign, and every approver sees it flagged. A transfer policy is Operational_Expense and a call policy Treasury_Management unless --type says otherwise. It signs under the pinned domain and company, with the BTC key's Cofund leaf on a Bitcoin vault. The policy activates at once when one admin may activate it; otherwise it awaits the other admins as a policy request.
  • invite create asks which user id the email's seat registers (an existing account keeps its own), signs InviteUser on each of the cofund's EVM vault chains (or --chains), 29 days out, and sends the invite. A member seat is claimable at once; an admin seat awaits the other admins' co-signatures where the chain asks for them, one invite request per chain.
  • Both show what they will sign and ask, or take --yes.

stocks buy (USDG in) and stocks sell (shares in, for USDG) quote the pool, set the minimum output 1% under the quote, show the swap and ask (or take --yes), sign it and propose it on the cofund's Stocks policy. The other signers approve it in the app or with requests sign.

SDK

import {
  btcMessageSigner,
  CofundClient,
  evmWallet,
  readKeystore,
  signRequest,
} from "getcofund";

const api = new CofundClient({ baseUrl, apiKey });
const me = await api.me();

// Sign what awaits you and matches your intent; refuse the rest unsigned.
const keystore = readKeystore(path);
for (const request of await api.requests(cofundId)) {
  const signature = await signRequest(
    request,
    {
      company: keystore.cofunds[cofundId].company,
      approve: (decoded) => decoded.kind === "transfer" && decoded.amount === "1000000",
    },
    pins,
    { evm: evmWallet(keystore), btc: btcMessageSigner(keystore) },
  ).catch(() => null); // not approved, or not what its fields say
  if (signature) {
    await api.signRequest(cofundId, request.id, signature);
  }
}

proposeTransfer and proposeCall take an id the caller picks (id, authId); a retry with the same id returns the same request, so the client retries them on a lost response. Either may carry expiresAt, an ISO 8601 deadline for its approvals at least a minute and at most 30 days out, a Bitcoin transfer too: past it no signature, partial or lock is taken (410), and a request still collecting is removed (a Bitcoin one's UTXOs freed), which a subscribed agent hears as request.failed with reason "expired". signTransferProposal and signCallProposal sign the proposer's own intent. A transfer's recipientEmail is optional: with it the recipient gets a receipt and becomes a contact. verifyRequest runs the checks signRequest runs and returns each result instead of throwing (what cofund requests show prints); checkToken holds a server's token to the SDK's pins (MAINNET_TOKENS, by chain id); parseChainPins reads development chain pins, refusing a mainnet chain id with another State.

A call is read before anything signs it, the proposer's own included. The SDK decodes the StockSwap wrapper itself; for any other wrapper, such as your own contract, pass a decoder for it in wrappers. It receives the raw instructions and allowances, returns (or resolves to) what the call does, handed to approve as decoded, or throws (or rejects) to refuse. Without one the call is refused. A policy activation on an unlisted contract (protocol.listed false, which approve sees) is approved only when that wrapper is in wrappers.

import { decodeAbiParameters, parseAbiParameters } from "viem";

const wrappers = {
  [myWrapper]: ({ instructions, allowances }) => {
    const [token, to, amount] = decodeAbiParameters(parseAbiParameters("address, address, uint256"), instructions);
    if (BigInt(allowances[0]?.amount ?? 0) < amount) throw new Error("over the allowance");
    return { token, to, amount };
  },
};
await signCallProposal(wallet, pins, { chain, company, policy, authId, wrapper: myWrapper, instructions, allowances }, { wrappers });
await signRequest(request, { company, wrappers, approve: (d) => d.kind === "call" && d.decoded.to === payee }, pins, signers);

A call request carries Cofund's simulation of it (simulation): its effect if it ran now, with the signature checks skipped by one State storage override (the policy's threshold set to 0), as a success with every balance it changes (the vault's, the wrapper's, anyone else's: signed base units), a revert with its reason, or unavailable. approve gets it, and vaultChanges(simulation) (getcofund/simulate) gives the vault's own changes, so an agent can refuse a call whose outflow or recipients are not its intent even when its decoder agrees. A read by id (api.request) takes it again when it is older than 15 seconds and the call still awaits signatures; when the chain cannot be read, the last good one stays, its at and block saying how old it is. incomplete means it also moved an NFT or more than 50 balances. It is a snapshot: state moves, and a contract can tell it is simulated and do otherwise when it runs. The most the vault can lose per asset is the allowance signed. After the last signature, thresholdSimulation is the exact relay with every signature, simulated before it is sent, and once executed executedEffect is what it did, from its own logs (native-coin moves leave no log).

To trust your own node instead of Cofund's, pass simulate: { rpc: { ETHEREUM: url }, from: wallet.address }: the call is simulated there with the same code (getcofund/simulate), the vault called directly from your address under the pinned State and company, and approve gets it as ownSimulation. Signing is refused when your node cannot simulate it, or when Cofund's disagrees (SimulationMismatchError from getcofund/simulate, with both).

import { vaultChanges } from "getcofund/simulate";

await signRequest(request, {
  company,
  wrappers,
  simulate: { rpc: { ETHEREUM: myNode }, from: wallet.address },
  approve: (d) =>
    d.kind === "call" &&
    d.ownSimulation?.outcome === "success" &&
    d.ownSimulation.changes.every((c) => c.role !== "other" || c.holder === payee) &&
    vaultChanges(d.ownSimulation).every((c) => BigInt(c.delta) >= -maxOut),
}, pins, signers);

A Bitcoin transfer (chain: "BTC") is approved with partials, not signRequest:

import { btcLeafSigner, signBtcTransfer, verifyBtcVault } from "getcofund";

const proposed = await api.proposeTransfer(cofundId, {
  id: crypto.randomUUID(),
  policyId,
  token: "BTC",
  recipient: "bc1q…",
  amount: "100000", // sats
  recipientEmail: "[email protected]",
});
// Once per vault: its definition must derive its address. Keep it.
const vault = verifyBtcVault(
  await api.vault(cofundId, proposed.vault.id),
  keystore.btc.network,
);
const locked = await api.lockRequest(cofundId, proposed.id); // the transaction
const partials = await signBtcTransfer(
  locked,
  vault,
  {
    approve: (d) => d.recipient === "bc1q…" && d.amount === "100000",
    limits: { maxFeeBps: 100, maxFeeRate: 1000 }, // the defaults
  },
  btcLeafSigner(keystore), // or your own BtcLeafSigner over an HSM
);
await api.signBtcRequest(cofundId, proposed.id, partials);

approve also receives spend: the vault's address, template, threshold and members, the Cofund fee, the change and the network fee it checked. verifyPinnedBtcVault(vault, keystore) is verifyBtcVault under the keystore's network that also refuses a vault whose address or definition differs from the one pinVault pinned. getcofund/btc-vault is the vault builder itself, the one the app builds vaults with. createBtcVault(cofundId, {vaultType, memberIds, threshold}) makes the cofund's Bitcoin vault (admins).

An admin's writes work the same way: signPolicyCreation and signInviteCreation build the activation or each chain's InviteUser from your inputs and sign it locally, and createPolicy and createInvite send it under the id you picked. signCofundCreation and createCofund found a cofund; a retry with the same id returns it as it stands. signSetupCompletion and completeSetup finish its setup.

import { randomUUID } from "node:crypto";
import {
  btcOwnershipProof,
  companyIdentifierFor,
  signCofundCreation,
  signInviteCreation,
  signPolicyCreation,
} from "getcofund";

const company = keystore.cofunds[cofundId].company;
const signers = { evm: evmWallet(keystore), btc: btcMessageSigner(keystore) };
const [vault] = await api.vaults(cofundId);
const roster = await api.members(cofundId);

const policy = await signPolicyCreation(
  {
    id: randomUUID(),
    vault,
    name: "Payouts",
    type: "Operational_Expense",
    signers: roster.filter((m) => m.isAdmin),
    threshold: 2,
    token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    maxAmount: "1000000000", // 1,000 USDC per transfer, in base units
    recipients: [payee], // optional allowlist; absent: any recipient
  },
  company,
  pins,
  signers,
);
await api.createPolicy(cofundId, policy); // AWAITING_SIGNATURES, or active at once

// A call policy on your own contract: name it, and it is registered as unlisted.
const own = await signPolicyCreation(
  { id: randomUUID(), vault, name: "Payouts wrapper", type: "Treasury_Management",
    signers: roster.filter((m) => m.isAdmin), threshold: 2,
    wrapper: myWrapper, wrapperName: "Payout wrapper" },
  company,
  pins,
  signers,
);
await api.createPolicy(cofundId, own);

const id = randomUUID();
const { userId } = await api.inviteUserId(cofundId, "[email protected]");
const invite = await signInviteCreation(
  {
    id,
    callerId: roster.find((m) => m.id === me.id).onchainId,
    userId: userId ?? id,
    email: "[email protected]",
    firstName: "Nia",
    lastName: "Okafor",
    position: "Treasurer",
    isAdmin: false,
    chains: ["ETHEREUM", "ROBINHOOD"],
  },
  company,
  pins,
  signers.evm,
);
await api.createInvite(cofundId, invite);

// Found a cofund: your own seat on each chain, and your BTC key.
const founded = await api.createCofund(
  await signCofundCreation(
    {
      id: randomUUID(),
      founderId: me.id,
      name: "Acme Corp",
      // "acme-corp", behind the deployment's namespace if it has one.
      company: companyIdentifierFor("Acme Corp", me.companyNamespace),
      chains: ["ETHEREUM", "ROBINHOOD"],
    },
    pins,
    evmWallet(keystore),
    btcOwnershipProof(keystore, me.id),
  ),
);
// founded.vaults[i].status is SUCCESS once its createClient lands.

In the browser

A web app imports from getcofund/browser: everything above except the keystore (generateKeystore, readKeystore, evmWallet, btcLeafSigner and the rest), so its bundle reaches no Node built-in. It signs with the user's own wallet: a viem account, or a viem wallet client through walletSigner from @getcofund/protocol/evm (any { address, signTypedData({ domain, types, primaryType, message }) }, such as an embedded wallet), and for Bitcoin a BtcMessageSigner or BtcLeafSigner of its own. Keep the API key on your server: send it the signatures and call the API there.

import { checkInvite, signInviteAcceptance } from "getcofund/browser";

const evm = await signInviteAcceptance(checkInvite(invite, { userId, company }, pins), wallet);

Managed users (app keys)

An app key, issued as one by a platform admin, lets an app act for many people of its own. cofund users create --external-id <id> --name <name>, or api.createUser({ externalId, name }) (the sign scope), creates the user the app manages under its own id for the person; the same id again returns the same user. It returns { id, email, externalId, name }. The generated email takes no mail; admins invite the user by it.

new CofundClient({ baseUrl, apiKey, actAs: user.id }), or COFUND_USER=<id> for the CLI, sends Cofund-User: <id>: every call is then that user's, with the key's scopes. Each managed user has keys of its own, generated by the app or the person's device (cofund keys init with a keystore per user), and joins or founds cofunds like any member. A user another app manages, or an unknown one, is 403. The founding limit counts the app and all its users together. A managed user has no Cofund login and cannot revoke the app; the app decides when it stops acting for them.

Webhooks

cofund webhooks add https://bot.example.com/cofund subscribes an endpoint for your key's agent, to every event or those named with --events, and prints its signing secret once. The URL must be https, on a host that resolves only to public addresses.

| Event | When | |---|---| | request.signature_needed | A transfer, call, policy activation or admin invite awaits your signature | | request.executed | It went through: executed on chain; a Bitcoin policy once approved; an admin invite's chain row once its last co-signature lands | | request.failed | It failed on chain; or its simulation reverted and nothing was sent, and it stays open; or it expired unsent (a swap's quote, an admin invite, a request past its expiresAt). reason says why | | request.rejected | An admin rejected it | | invite.received | You were invited to a cofund (inviteId) | | deposit.received | A payment into one of the cofund's vaults confirmed (depositId); every member's agent hears it |

The body is {id, type, createdAt, cofundId} and, by type: every request.* event its requestId and kind (transfer, call, policy or invite), request.failed an optional reason, invite.received its inviteId. An app key's subscriptions also get the events of the users it manages, with userIds naming them. Never amounts or addresses: fetch the request with api.request(cofundId, requestId) and check it as signRequest does. WebhookEventSchema parses it, narrowed by type. Delivery is at least once and in no particular order. Answer 2xx within 10 seconds; anything else is retried at 1m, 5m, 30m, 2h, 6h and 24h, and three days of failures disable the subscription (your key's owner is emailed).

For deposit.received, api.deposits(cofundId) lists the cofund's deposits newest first, each with its vault, sender (from, null when the chain does not tell), token, amount in base units, txid, status and times; { limit, before } page through them. Mapping a sender to a person is the app's to do.

import { createServer } from "node:http";
import { verifyWebhookSignature, WebhookEventSchema } from "getcofund";

const seen = new Set(); // use your database in production

createServer((req, res) => {
  let rawBody = "";
  req.setEncoding("utf8");
  req.on("data", (chunk) => (rawBody += chunk));
  req.on("end", async () => {
    // The exact bytes received, before any JSON parsing; refuses a stale timestamp.
    if (!verifyWebhookSignature(req.headers["cofund-signature"], rawBody, process.env.COFUND_WEBHOOK_SECRET)) {
      res.writeHead(401).end();
      return;
    }
    const event = WebhookEventSchema.parse(JSON.parse(rawBody));
    res.writeHead(200).end(); // acknowledge first, then work
    if (seen.has(event.id)) return; // a retry of one already handled
    seen.add(event.id);
    if (event.type === "request.signature_needed") {
      const request = await api.request(event.cofundId, event.requestId);
      // ...decide, then sign it as in the SDK example above
    } else if (event.type === "invite.received") {
      // ...accept event.inviteId with `cofund invite accept --invite <id>`
    }
  });
}).listen(8080);

Node 20 or later, ESM.

Plain HTTP

People read the docs at /developers, built from the two documents below. An agent that cannot run Node reads them on the API itself, with no key:

  • /api/v1/llms.txt: the flow, and exactly what to check before signing each kind of request without this SDK.
  • /api/v1/openapi.json: every endpoint, its scope, body, response and errors, built from this package's api-types.