countersignatory
v0.2.0
Published
The going rate for a human minute. A dependency-free client for the Countersignatory quote API, Spot Index and orders.
Maintainers
Readme
countersignatory
The going rate for a human minute. A JavaScript and TypeScript client for the Countersignatory quote API, Spot Index and orders. No dependencies, global fetch, Node 18 and later, ESM with types.
Most prices are indicative. A quote is not a binding offer. The markets the index marks live can be bought with an order, paid for, and answered by hand by a named group; everywhere else no task is taken and no money moves.
Quickstart
import { quote, interest, index, market } from "countersignatory";
const q = await quote({ task: "approval", tier: "check", sla: 900, maxPrice: 2 });
console.log(q.unit_price, q.band, q.would_clear, q.quote_id);
await interest({ quote: q, wouldPay: true, note: "labelling 200 eval rows a week" }); // this price would clear
await index(); // the Spot Index
await market("countersign.certified_translation"); // one market from it
await interest({ market: "countersign.certified_translation", wouldPay: true }); // vote to open a marketArguments map one-to-one onto POST /v1/quotes: task is task_type, sla is sla_seconds, n is consensus_n, maxPrice is max_price, market is market_id, and jurisdiction, unassisted and criteria keep their names. The quote comes back exactly as the API sent it, with derived carrying what is derived rather than measured. Nothing is rounded.
Buy a Check
A market whose state is live on the index can be bought. check.general is one screened human, $1.25 at a 24 hour deadline, answered by hand or refunded in full.
import { order, payX402, waitForAnswer } from "countersignatory";
import { createSigner } from "x402-fetch"; // optional: npm i x402-fetch
const o = await order({
market: "check.general", template: "approval", sla: 86400, maxPrice: 1.25,
input: { action: "Send the refund of $40 to customer 1182", context: "The item arrived broken. Policy allows refunds under $50." },
}); // unpaid: nothing is charged yet
const signer = await createSigner("base", process.env.BUYER_PRIVATE_KEY as `0x${string}`);
const { transaction } = await payX402(o, signer); // 1.25 USDC on Base, and no more
const done = await waitForAnswer(o.id, { timeout: 3600 }); // polls every 15 seconds
console.log(done.answer.value, done.answer.reason, done.answer.responder.disclosed_as, transaction);order() never pays by itself. It returns the order with both ways to pay under payment_options: x402.pay_url for an agent with a wallet, and card.checkout_url for a person. Pay within 30 minutes, by payX402() or by handing checkout_url to your human.
payX402(order, signer) is the only call that moves money, and it runs only when you call it. It needs x402-fetch, an optional peer dependency this package does not install. The signer is what createSigner(network, privateKey) returns, or a viem wallet client. The key stays in your process, and the most it will sign for is the order's own price. The buyer needs USDC on Base and no ETH, because the facilitator pays the gas. A payment the server does not accept throws payment_not_accepted and the order stays unpaid. If you sign an X-PAYMENT header some other way, pass it to order() as payment instead.
waitForAnswer(id, { timeout, interval }) reads the order until it is answered, refused, refunded or expired, and returns it. Both are in seconds, default 3600 and 15. If the timeout passes first it throws wait_timeout with the last order read as body; the order keeps its own deadline. getOrder(id) reads it once. Neither needs a key: the order id is the credential.
Limits while the founder answers alone
While a live market's only responders are founders, and the index says so under fulfilment.limits:
slamust be at least 3600 seconds.- An order placed between 23:00 and 07:00 Europe/London needs a deadline at or after 08:00 Europe/London.
An order outside those limits is a 422 with the reason, raised as an error. Nothing is charged and no order is made.
Refused at intake
Refused with a 422, refused_at_intake, before any payment:
- solving CAPTCHAs or passing any bot check
- logging in to anything, or entering credentials
- contacting third parties
- medical, legal or financial advice
- anything sexual, or involving minors
A responder may also refuse after intake, which refunds you in full. Templates are approval, verification and judgment; llms.txt gives the input each one takes and a worked x402 example.
Keys
A key is free, instant and needs no approval. The client looks for one in this order:
keyon the call.COUNTERSIGNATORY_KEYin the environment.- The cached file
~/.config/countersignatory/key(%APPDATA%\countersignatory\keyon Windows), mode 0600. - In CI (
CI,GITHUB_ACTIONS,GITLAB_CI,BUILDKITE,CIRCLECIorJENKINS_URLset), it throwsKeyRequired. It never issues a key in CI, because every ephemeral runner would mint a new one. SetCOUNTERSIGNATORY_KEYin your CI secrets. - Otherwise it takes a key with
POST /v1/keys, caches it and prints one line to stderr saying where.
index() and market() need no key.
Errors
Nothing is retried and nothing is substituted. An error from the API is thrown as CountersignatoryError with code, message, status and body. Quoting a market that is listed but not open throws MarketNotOpen, whose message carries the one line that registers interest in it. A key that is not in the shape the API issues throws InvalidKey.
What it sends
Each request carries x-countersignatory-client: js/0.2.0, so calls from this client can be counted as a channel. That header is the only telemetry. Nothing is sent on import.
Set COUNTERSIGNATORY_BASE_URL to point the client somewhere other than https://countersignatory.com.
Markets
Indicative prices from the Spot Index, index version seed-2026-10-04, read when this package was built. They are seeded from third-party comparables, not live clears, and do not update with the package. Call index() for the current figures and /methodology for how each one is derived.
| Market | Tier | State | Indicative base (USD) | Band | Unit |
|---|---|---|---|---|---|
| check.general | check | open | $1.25 | $0.80 to $2.00 | per task |
| consensus.3.general | consensus | open | $4.70 | $2.60 to $5.00 | per task |
| consensus.5.general | consensus | open | $5.60 | $4.20 to $8.00 | per task |
| countersign.domain_expert | countersign | open | $18.00 | $12.00 to $30.00 | per attestation |
| countersign.uk_professional | countersign | open | $25.00 | $15.00 to $40.00 | per attestation |
| countersign.accessibility_statement | countersign | opening | $21.00 | $14.00 to $28.00 | per criterion (MED shown) |
| countersign.certified_translation | countersign | opening | $16.00 | $12.00 to $24.00 | per page (250 words) |
| seal.notarisation_us | seal | open | $45.00 | $38.00 to $60.00 | per seal |
A market in the opening state is listed with a reference rate but takes no quotes yet. Quoting it raises market_not_open; registering interest is the vote that opens it. A market the seed marks for sale is served as live only while a payment rail and its responders stand behind it, and as open otherwise, so call index() for the state now.
Methodology: https://countersignatory.com/methodology
MIT licence. Countersignatory Ltd.
