countersignatory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +244 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Countersignatory Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# countersignatory
|
|
2
|
+
|
|
3
|
+
The going rate for a human minute. A JavaScript and TypeScript client for the Countersignatory quote API and Spot Index. No dependencies, global `fetch`, Node 18 and later, ESM with types.
|
|
4
|
+
|
|
5
|
+
**Nothing is fulfilled yet.** Every price is an indicative quote, not a binding offer. No task is taken, no work is done and no money moves.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { quote, interest, index, market } from "countersignatory";
|
|
11
|
+
|
|
12
|
+
const q = await quote({ task: "approval", tier: "check", sla: 900, maxPrice: 1.5 });
|
|
13
|
+
console.log(q.unit_price, q.band, q.would_clear, q.quote_id);
|
|
14
|
+
|
|
15
|
+
await interest({ quote: q, wouldPay: true, note: "labelling 200 eval rows a week" }); // this price would clear
|
|
16
|
+
await index(); // the Spot Index
|
|
17
|
+
await market("countersign.certified_translation"); // one market from it
|
|
18
|
+
await interest({ market: "countersign.certified_translation", wouldPay: true }); // vote to open a market
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Arguments 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.
|
|
22
|
+
|
|
23
|
+
## Keys
|
|
24
|
+
|
|
25
|
+
A key is free, instant and needs no approval. The client looks for one in this order:
|
|
26
|
+
|
|
27
|
+
1. `key` on the call.
|
|
28
|
+
2. `COUNTERSIGNATORY_KEY` in the environment.
|
|
29
|
+
3. The cached file `~/.config/countersignatory/key` (`%APPDATA%\countersignatory\key` on Windows), mode 0600.
|
|
30
|
+
4. In CI (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI` or `JENKINS_URL` set), it throws `KeyRequired`. It never issues a key in CI, because every ephemeral runner would mint a new one. Set `COUNTERSIGNATORY_KEY` in your CI secrets.
|
|
31
|
+
5. Otherwise it takes a key with `POST /v1/keys`, caches it and prints one line to stderr saying where.
|
|
32
|
+
|
|
33
|
+
`index()` and `market()` need no key.
|
|
34
|
+
|
|
35
|
+
## Errors
|
|
36
|
+
|
|
37
|
+
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`.
|
|
38
|
+
|
|
39
|
+
## What it sends
|
|
40
|
+
|
|
41
|
+
Each request carries `x-countersignatory-client: js/0.1.0`, so calls from this client can be counted as a channel. That header is the only telemetry. Nothing is sent on import.
|
|
42
|
+
|
|
43
|
+
Set `COUNTERSIGNATORY_BASE_URL` to point the client somewhere other than `https://countersignatory.com`.
|
|
44
|
+
|
|
45
|
+
## Markets
|
|
46
|
+
|
|
47
|
+
<!-- prices:start -->
|
|
48
|
+
Indicative prices from the Spot Index, index version `seed-2026-09-23b`, 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.
|
|
49
|
+
|
|
50
|
+
| Market | Tier | State | Indicative base (USD) | Band | Unit |
|
|
51
|
+
|---|---|---|---|---|---|
|
|
52
|
+
| `check.general` | check | open | $0.80 | $0.50 to $1.50 | per task |
|
|
53
|
+
| `consensus.3.general` | consensus | open | $3.50 | $2.60 to $5.00 | per task |
|
|
54
|
+
| `consensus.5.general` | consensus | open | $5.60 | $4.20 to $8.00 | per task |
|
|
55
|
+
| `countersign.domain_expert` | countersign | open | $18.00 | $12.00 to $30.00 | per attestation |
|
|
56
|
+
| `countersign.uk_professional` | countersign | open | $25.00 | $15.00 to $40.00 | per attestation |
|
|
57
|
+
| `countersign.accessibility_statement` | countersign | opening | $21.00 | $14.00 to $28.00 | per criterion (MED shown) |
|
|
58
|
+
| `countersign.certified_translation` | countersign | opening | $16.00 | $12.00 to $24.00 | per page (250 words) |
|
|
59
|
+
| `seal.notarisation_us` | seal | open | $45.00 | $38.00 to $60.00 | per seal |
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
<!-- prices:end -->
|
|
63
|
+
|
|
64
|
+
Methodology: https://countersignatory.com/methodology
|
|
65
|
+
|
|
66
|
+
MIT licence. Countersignatory Ltd.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0";
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "https://countersignatory.com";
|
|
3
|
+
/** An error from the API, or from this client refusing to guess. */
|
|
4
|
+
export declare class CountersignatoryError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly status?: number | undefined;
|
|
7
|
+
readonly body?: unknown | undefined;
|
|
8
|
+
constructor(code: string, message: string, status?: number | undefined, body?: unknown | undefined);
|
|
9
|
+
}
|
|
10
|
+
/** The market is listed but has no supply yet, so it cannot be quoted. */
|
|
11
|
+
export declare class MarketNotOpen extends CountersignatoryError {
|
|
12
|
+
readonly marketId: string | null;
|
|
13
|
+
readonly registerInterest: string | null;
|
|
14
|
+
constructor(message: string, marketId: string | null, status?: number, body?: unknown);
|
|
15
|
+
}
|
|
16
|
+
/** No key was found, and this client will not issue one here. */
|
|
17
|
+
export declare class KeyRequired extends CountersignatoryError {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
20
|
+
/** A key is not in the shape the API issues, or the API never issued it. */
|
|
21
|
+
export declare class InvalidKey extends CountersignatoryError {
|
|
22
|
+
constructor(message: string, status?: number, body?: unknown);
|
|
23
|
+
}
|
|
24
|
+
export type TaskType = "judgment" | "verification" | "approval" | "physical" | "signature";
|
|
25
|
+
export type Tier = "check" | "consensus" | "countersign" | "seal";
|
|
26
|
+
export interface Criterion {
|
|
27
|
+
id: string;
|
|
28
|
+
status?: "automated-fail" | "automated-review" | "not-flagged" | "not-run" | "manual-only" | "agent-verified";
|
|
29
|
+
measured?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface QuoteArgs {
|
|
32
|
+
/** task_type */
|
|
33
|
+
task: TaskType;
|
|
34
|
+
/** sla_seconds */
|
|
35
|
+
sla: number;
|
|
36
|
+
tier?: Tier;
|
|
37
|
+
/** consensus_n */
|
|
38
|
+
n?: number;
|
|
39
|
+
/** max_price */
|
|
40
|
+
maxPrice?: number;
|
|
41
|
+
/** market_id */
|
|
42
|
+
market?: string;
|
|
43
|
+
jurisdiction?: string;
|
|
44
|
+
unassisted?: boolean;
|
|
45
|
+
criteria?: Criterion[];
|
|
46
|
+
key?: string;
|
|
47
|
+
}
|
|
48
|
+
/** The quote exactly as the API returned it. Values are passed through unrounded. */
|
|
49
|
+
export interface Quote {
|
|
50
|
+
quote_id: string;
|
|
51
|
+
market: Record<string, unknown> & {
|
|
52
|
+
id: string;
|
|
53
|
+
tier: Tier;
|
|
54
|
+
state: string;
|
|
55
|
+
};
|
|
56
|
+
currency: string;
|
|
57
|
+
unit_price: number;
|
|
58
|
+
band: [number, number];
|
|
59
|
+
multipliers_applied: Record<string, number>;
|
|
60
|
+
compound_factor: number;
|
|
61
|
+
derived: {
|
|
62
|
+
implied_price_per_human_minute: number;
|
|
63
|
+
implied_responder_hourly: number;
|
|
64
|
+
responder_take_home: number;
|
|
65
|
+
effort_minutes?: number;
|
|
66
|
+
clears_wage_floor: boolean;
|
|
67
|
+
note: string;
|
|
68
|
+
};
|
|
69
|
+
lines?: unknown[];
|
|
70
|
+
class_table?: string;
|
|
71
|
+
would_clear: boolean | null;
|
|
72
|
+
alternatives: Array<{
|
|
73
|
+
change: string;
|
|
74
|
+
unit_price: number;
|
|
75
|
+
would_clear: boolean;
|
|
76
|
+
}>;
|
|
77
|
+
next: {
|
|
78
|
+
register_interest: string;
|
|
79
|
+
};
|
|
80
|
+
sla_seconds: number;
|
|
81
|
+
method: string;
|
|
82
|
+
price_inputs_ignored: string[];
|
|
83
|
+
backstop: {
|
|
84
|
+
available: boolean;
|
|
85
|
+
note: string;
|
|
86
|
+
};
|
|
87
|
+
expires_at: string;
|
|
88
|
+
disclaimer: string;
|
|
89
|
+
[field: string]: unknown;
|
|
90
|
+
}
|
|
91
|
+
export interface InterestArgs {
|
|
92
|
+
/** A quote you were shown, or its quote_id. Exactly one of this or market. */
|
|
93
|
+
quote?: Quote | string;
|
|
94
|
+
/** A market that is not open yet. Exactly one of this or quote. */
|
|
95
|
+
market?: string;
|
|
96
|
+
wouldPay?: boolean;
|
|
97
|
+
note?: string;
|
|
98
|
+
key?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface IndexMarket {
|
|
101
|
+
id: string;
|
|
102
|
+
title: string;
|
|
103
|
+
tier: Tier;
|
|
104
|
+
state: string;
|
|
105
|
+
base: number;
|
|
106
|
+
band: [number, number];
|
|
107
|
+
unit: string;
|
|
108
|
+
[field: string]: unknown;
|
|
109
|
+
}
|
|
110
|
+
export interface SpotIndex {
|
|
111
|
+
version: string;
|
|
112
|
+
currency: string;
|
|
113
|
+
markets: IndexMarket[];
|
|
114
|
+
[field: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Price one task. Arguments map one-to-one onto the POST /v1/quotes body: task
|
|
118
|
+
* is task_type, sla is sla_seconds, n is consensus_n, maxPrice is max_price and
|
|
119
|
+
* market is market_id. A market that is listed but not open throws MarketNotOpen.
|
|
120
|
+
*/
|
|
121
|
+
export declare function quote(args: QuoteArgs): Promise<Quote>;
|
|
122
|
+
/**
|
|
123
|
+
* Record that a quoted price would clear (pass the quote or its quote_id), or
|
|
124
|
+
* that a market that is not open yet is wanted (pass market). Exactly one.
|
|
125
|
+
*/
|
|
126
|
+
export declare function interest(args: InterestArgs): Promise<{
|
|
127
|
+
ok: boolean;
|
|
128
|
+
message: string;
|
|
129
|
+
}>;
|
|
130
|
+
/** The Spot Index, as published. Needs no key. */
|
|
131
|
+
export declare function index(): Promise<SpotIndex>;
|
|
132
|
+
/** One market from the Spot Index. An id that is not listed is an error. */
|
|
133
|
+
export declare function market(marketId: string): Promise<IndexMarket>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Countersignatory: the going rate for a human minute.
|
|
3
|
+
*
|
|
4
|
+
* A thin client for the Countersignatory quote API. No dependencies: global
|
|
5
|
+
* fetch, Node 18 or later.
|
|
6
|
+
*
|
|
7
|
+
* import { quote } from "countersignatory";
|
|
8
|
+
* const q = await quote({ task: "approval", tier: "check", sla: 900, maxPrice: 1.5 });
|
|
9
|
+
*
|
|
10
|
+
* Quotes are indicative. Nothing is fulfilled yet.
|
|
11
|
+
*
|
|
12
|
+
* - Every request carries one header, x-countersignatory-client: js/<version>.
|
|
13
|
+
* There is no other telemetry, and nothing is sent on import.
|
|
14
|
+
* - Errors are thrown as they come back. Nothing is retried, and no
|
|
15
|
+
* neighbouring tier or market is ever substituted for the one asked for.
|
|
16
|
+
* - The quote is returned as the server sent it. Nothing is rounded.
|
|
17
|
+
* - A free key is issued on first use and cached, except in CI, where you must
|
|
18
|
+
* set COUNTERSIGNATORY_KEY yourself.
|
|
19
|
+
*/
|
|
20
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
export const VERSION = "0.1.0";
|
|
24
|
+
export const DEFAULT_BASE_URL = "https://countersignatory.com";
|
|
25
|
+
const CLIENT = `js/${VERSION}`;
|
|
26
|
+
const TIMEOUT_MS = 30_000;
|
|
27
|
+
const KEY_FORMAT = /^csk_live_[0-9a-f]{32}$/;
|
|
28
|
+
const CI_VARS = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "JENKINS_URL"];
|
|
29
|
+
/** Keys resolved in this process, by base URL, so the cache file is read once. */
|
|
30
|
+
const resolved = new Map();
|
|
31
|
+
/* ------------------------------------------------------------------ errors */
|
|
32
|
+
/** An error from the API, or from this client refusing to guess. */
|
|
33
|
+
export class CountersignatoryError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
status;
|
|
36
|
+
body;
|
|
37
|
+
constructor(code, message, status, body) {
|
|
38
|
+
super(`${code}: ${message}`);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.body = body;
|
|
42
|
+
this.name = "CountersignatoryError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The market is listed but has no supply yet, so it cannot be quoted. */
|
|
46
|
+
export class MarketNotOpen extends CountersignatoryError {
|
|
47
|
+
marketId;
|
|
48
|
+
registerInterest;
|
|
49
|
+
constructor(message, marketId, status, body) {
|
|
50
|
+
const line = marketId ? `await interest({ market: "${marketId}", wouldPay: true })` : null;
|
|
51
|
+
super("market_not_open", line ? `${message}\n\nRegistering interest is the vote that opens it:\n\n ${line}` : message, status, body);
|
|
52
|
+
this.marketId = marketId;
|
|
53
|
+
this.name = "MarketNotOpen";
|
|
54
|
+
this.registerInterest = line;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** No key was found, and this client will not issue one here. */
|
|
58
|
+
export class KeyRequired extends CountersignatoryError {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super("key_required", message);
|
|
61
|
+
this.name = "KeyRequired";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A key is not in the shape the API issues, or the API never issued it. */
|
|
65
|
+
export class InvalidKey extends CountersignatoryError {
|
|
66
|
+
constructor(message, status, body) {
|
|
67
|
+
super("invalid_key", message, status, body);
|
|
68
|
+
this.name = "InvalidKey";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/* --------------------------------------------------------------- transport */
|
|
72
|
+
function baseUrl() {
|
|
73
|
+
return (process.env.COUNTERSIGNATORY_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
74
|
+
}
|
|
75
|
+
/** @internal Exported for tests. */
|
|
76
|
+
export async function request(method, path, body, key) {
|
|
77
|
+
const headers = { accept: "application/json", "x-countersignatory-client": CLIENT };
|
|
78
|
+
if (body !== undefined)
|
|
79
|
+
headers["content-type"] = "application/json";
|
|
80
|
+
if (key)
|
|
81
|
+
headers.authorization = `Bearer ${key}`;
|
|
82
|
+
const res = await fetch(baseUrl() + path, {
|
|
83
|
+
method,
|
|
84
|
+
headers,
|
|
85
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
86
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
const text = await res.text();
|
|
89
|
+
let payload = text;
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(text);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not JSON: surfaced as-is below.
|
|
95
|
+
}
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const error = payload && typeof payload === "object" ? payload.error ?? {} : {};
|
|
98
|
+
const message = error.message ?? (error.issues ? JSON.stringify(error.issues) : `HTTP ${res.status} from ${path}`);
|
|
99
|
+
if (error.code === "invalid_key")
|
|
100
|
+
throw new InvalidKey(message, res.status, payload);
|
|
101
|
+
throw new CountersignatoryError(error.code ?? `http_${res.status}`, message, res.status, payload);
|
|
102
|
+
}
|
|
103
|
+
return payload;
|
|
104
|
+
}
|
|
105
|
+
/* -------------------------------------------------------------------- keys */
|
|
106
|
+
function configDir() {
|
|
107
|
+
if (process.platform === "win32" && process.env.APPDATA)
|
|
108
|
+
return join(process.env.APPDATA, "countersignatory");
|
|
109
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "countersignatory");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The default server uses the documented path. Any other server gets its own
|
|
113
|
+
* file, so a key issued by a local test server is never sent to production,
|
|
114
|
+
* where it would be silently unrecognised.
|
|
115
|
+
*/
|
|
116
|
+
function cachePath(base) {
|
|
117
|
+
if (base === DEFAULT_BASE_URL)
|
|
118
|
+
return join(configDir(), "key");
|
|
119
|
+
let host = "unknown";
|
|
120
|
+
try {
|
|
121
|
+
host = new URL(base).host.replace(/[^A-Za-z0-9.-]/g, "_") || "unknown";
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Left as unknown.
|
|
125
|
+
}
|
|
126
|
+
return join(configDir(), `key@${host}`);
|
|
127
|
+
}
|
|
128
|
+
function inCI() {
|
|
129
|
+
return CI_VARS.some((name) => {
|
|
130
|
+
const v = (process.env[name] ?? "").trim().toLowerCase();
|
|
131
|
+
return v !== "" && v !== "0" && v !== "false" && v !== "no";
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function checked(key, source) {
|
|
135
|
+
const k = key.trim();
|
|
136
|
+
if (!KEY_FORMAT.test(k)) {
|
|
137
|
+
throw new InvalidKey(`The key from ${source} is not a Countersignatory key. Keys look like csk_live_ followed by 32 hex characters.`);
|
|
138
|
+
}
|
|
139
|
+
return k;
|
|
140
|
+
}
|
|
141
|
+
async function resolveKey(explicit) {
|
|
142
|
+
if (explicit !== undefined)
|
|
143
|
+
return checked(explicit, "the key argument");
|
|
144
|
+
const env = process.env.COUNTERSIGNATORY_KEY;
|
|
145
|
+
if (env)
|
|
146
|
+
return checked(env, "COUNTERSIGNATORY_KEY");
|
|
147
|
+
const base = baseUrl();
|
|
148
|
+
const memo = resolved.get(base);
|
|
149
|
+
if (memo)
|
|
150
|
+
return memo;
|
|
151
|
+
const path = cachePath(base);
|
|
152
|
+
let cached = null;
|
|
153
|
+
try {
|
|
154
|
+
cached = await readFile(path, "utf8");
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (err?.code !== "ENOENT")
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
if (cached !== null) {
|
|
161
|
+
const key = checked(cached, path);
|
|
162
|
+
resolved.set(base, key);
|
|
163
|
+
return key;
|
|
164
|
+
}
|
|
165
|
+
if (inCI()) {
|
|
166
|
+
throw new KeyRequired("CI detected, so no key is issued automatically: every ephemeral runner would mint a new one. " +
|
|
167
|
+
"Take a free key once with POST /v1/keys and set it as COUNTERSIGNATORY_KEY in your CI secrets.");
|
|
168
|
+
}
|
|
169
|
+
const issued = await request("POST", "/v1/keys", {});
|
|
170
|
+
const key = checked(issued.key, "POST /v1/keys");
|
|
171
|
+
resolved.set(base, key);
|
|
172
|
+
try {
|
|
173
|
+
await mkdir(configDir(), { recursive: true, mode: 0o700 });
|
|
174
|
+
await writeFile(path, key + "\n", { mode: 0o600 });
|
|
175
|
+
await chmod(path, 0o600);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
process.stderr.write(`countersignatory: issued a free key but could not save it to ${path} (${err?.code ?? err}). ` +
|
|
179
|
+
"It is used for this process only. Set COUNTERSIGNATORY_KEY to use your own.\n");
|
|
180
|
+
return key;
|
|
181
|
+
}
|
|
182
|
+
process.stderr.write(`countersignatory: issued a free key, saved to ${path}. Set COUNTERSIGNATORY_KEY to use your own.\n`);
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
/* ----------------------------------------------------------------- surface */
|
|
186
|
+
/**
|
|
187
|
+
* Price one task. Arguments map one-to-one onto the POST /v1/quotes body: task
|
|
188
|
+
* is task_type, sla is sla_seconds, n is consensus_n, maxPrice is max_price and
|
|
189
|
+
* market is market_id. A market that is listed but not open throws MarketNotOpen.
|
|
190
|
+
*/
|
|
191
|
+
export async function quote(args) {
|
|
192
|
+
const fields = {
|
|
193
|
+
task_type: args.task,
|
|
194
|
+
sla_seconds: args.sla,
|
|
195
|
+
tier: args.tier,
|
|
196
|
+
consensus_n: args.n,
|
|
197
|
+
max_price: args.maxPrice,
|
|
198
|
+
market_id: args.market,
|
|
199
|
+
jurisdiction: args.jurisdiction,
|
|
200
|
+
unassisted: args.unassisted,
|
|
201
|
+
criteria: args.criteria,
|
|
202
|
+
};
|
|
203
|
+
const body = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
204
|
+
const key = await resolveKey(args.key);
|
|
205
|
+
try {
|
|
206
|
+
return await request("POST", "/v1/quotes", body, key);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (err instanceof CountersignatoryError && err.code === "market_not_open") {
|
|
210
|
+
const message = err.body?.error?.message ?? err.message;
|
|
211
|
+
throw new MarketNotOpen(message, args.market ?? null, err.status, err.body);
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Record that a quoted price would clear (pass the quote or its quote_id), or
|
|
218
|
+
* that a market that is not open yet is wanted (pass market). Exactly one.
|
|
219
|
+
*/
|
|
220
|
+
export async function interest(args) {
|
|
221
|
+
if ((args.quote === undefined) === (args.market === undefined)) {
|
|
222
|
+
throw new CountersignatoryError("invalid_request", "Pass exactly one of quote (or its quote_id), for a price you were shown, or market, for a market that is not open yet.");
|
|
223
|
+
}
|
|
224
|
+
const body = args.quote !== undefined
|
|
225
|
+
? { quote_id: typeof args.quote === "string" ? args.quote : args.quote.quote_id }
|
|
226
|
+
: { market_id: args.market };
|
|
227
|
+
if (args.wouldPay !== undefined)
|
|
228
|
+
body.would_pay = args.wouldPay;
|
|
229
|
+
if (args.note !== undefined)
|
|
230
|
+
body.note = args.note;
|
|
231
|
+
return request("POST", "/v1/interest", body, await resolveKey(args.key));
|
|
232
|
+
}
|
|
233
|
+
/** The Spot Index, as published. Needs no key. */
|
|
234
|
+
export async function index() {
|
|
235
|
+
return request("GET", "/v1/index");
|
|
236
|
+
}
|
|
237
|
+
/** One market from the Spot Index. An id that is not listed is an error. */
|
|
238
|
+
export async function market(marketId) {
|
|
239
|
+
const found = (await index()).markets.find((m) => m.id === marketId);
|
|
240
|
+
if (!found) {
|
|
241
|
+
throw new CountersignatoryError("unknown_market", `No market '${marketId}' is listed on the Spot Index. Call index() for the list.`);
|
|
242
|
+
}
|
|
243
|
+
return found;
|
|
244
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "countersignatory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The going rate for a human minute. A dependency-free client for the Countersignatory quote API and Spot Index.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/index.js",
|
|
16
|
+
"dist/index.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"test": "node --test test/sdk.test.mjs",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"author": "Countersignatory Ltd <douglas@countersignatory.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://countersignatory.com",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"email": "douglas@countersignatory.com"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"countersignatory",
|
|
37
|
+
"human-in-the-loop",
|
|
38
|
+
"ai-agents",
|
|
39
|
+
"pricing",
|
|
40
|
+
"verification",
|
|
41
|
+
"spot-index"
|
|
42
|
+
],
|
|
43
|
+
"countersignatory": {
|
|
44
|
+
"methodology": "https://countersignatory.com/methodology"
|
|
45
|
+
}
|
|
46
|
+
}
|