Use Quickstart with TypeScript SDK for first setup. Use this page after connection to choose client methods, signing helpers, diagnostics, path helpers, and exports.
MCP users who do not already have a wallet or signing service can also use this page to set up the TypeScript SDK with viem as a local testnet signing harness for typed data prepared by Quickstart with MCP.
Use literal SDK symbols exactly as exported by the package, including ApiClient, AgreementsApiError, agreementsApiPaths, and API_BASE_PATH.
The package targets Node >=18.
Install
npm install @cns-labs/agreements-api-client viem
@cns-labs/agreements-protocol-evm is installed automatically as a package dependency. Install viem in your app when you use permit-signing helpers.
The Agreements API client and the onchain protocol SDK are both published under the @cns-labs npm organization. The agreements-api-playground sample application uses @cns-labs/agreements-api-client for API calls and @cns-labs/agreements-protocol-evm for onchain agreement typing and signing support.
Create a client
import { ApiClient } from '@cns-labs/agreements-api-client';
const client = new ApiClient({
environment: 'testnet',
apiKey: process.env.API_KEY,
});
const health = await client.getHealth();
Constructor config:
| Field | Use |
|---|
environment | Named API environment. Supported values are testnet and production. |
baseUrl | Optional API host base URL override, without /v0 appended. Use for local proxies, staging hosts, or custom deployments. |
apiKey | Optional X-API-Key value. Most API methods require it. |
headers | Optional header record or header factory merged into each request. |
fetch | Optional fetch implementation. Defaults to globalThis.fetch. |
If both environment and baseUrl are supplied, baseUrl wins. testnet resolves to https://test-api.shodai.network; production resolves to https://api.shodai.network.
For key provisioning, scopes, entitlements, and 401/402/403 behavior, see Authentication.
Wallet and RPC requirements
The API key authenticates requests. Deploying agreements and submitting agreement inputs also require a wallet your integration controls because those workflows depend on EIP-712 signatures.
| Requirement | Why it matters |
|---|
walletClient | Controls the signing account and produces EIP-712 signatures. A plain wallet address is not enough. |
publicClient | Reads target-chain context, factory or agreement contract state, and current permit nonces. |
| Target chain/RPC | Must match the selected deployment chain or the deployed agreement’s chain. |
| Signer eligibility | For inputs, the signing wallet must be allowed by the authored input issuer. |
Create a test-only wallet client
For automated tests that only need EIP-712 signatures, create an ephemeral wallet with viem. This wallet does not need gas when it only signs permits. Do not use this pattern for production wallets, and do not commit generated private keys.
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { lineaSepolia } from 'viem/chains';
const account = privateKeyToAccount(generatePrivateKey());
const walletClient = createWalletClient({
account,
chain: lineaSepolia,
transport: http(process.env.RPC_URL),
});
const publicClient = createPublicClient({
chain: lineaSepolia,
transport: http(process.env.RPC_URL),
});
The testnet API environment supports Linea Sepolia (59141), Ethereum Sepolia (11155111), and Base Sepolia (84532) for agreement deployments. The production API environment supports Linea Mainnet (59144) and Base Mainnet (8453). Choose the chainId explicitly for deployment preflight and deploy requests, then reuse the deployed agreement record’s chainId when signing inputs.
Use account.address in participant mappings when the test wallet needs to deploy an agreement or submit an input for a participant role.
Before calling deployAgreementWithPermit(...) or submitAgreementInputWithPermit(...), confirm that:
- you have an API key for authenticated requests
- you have a
walletClient that can sign with the intended account
- you have a
publicClient connected to the target chain
- the wallet chain matches the deployment or agreement chain
- the signer is appropriate for the deployment or input issuer
- you can regenerate signatures when nonce, deadline, payload, chain, agreement JSON, or values change
Choose the right SDK surface
| Use case | Prefer |
|---|
| Normal API calls | ApiClient methods such as validateTemplate, validateDeployment, and getAgreementState. |
| Deploy with an EIP-712 permit | deployAgreementWithPermit(...). |
| Submit a signed input | submitAgreementInputWithPermit(...). |
| Sign first, submit later, or customize request composition | signDeployWithPermit(...) or signAgreementInputPermit(...) plus client.deployWithPermit(...) or client.submitAgreementInput(...). |
| Debug HTTP status, headers, or raw body text | client.exchangeJson(...). |
| Compose a raw request path safely | agreementsApiPaths. |
| Construct typed data manually without SDK helpers | EIP-712 Signing Reference. |
Main client methods
The public API returns JSON envelopes for authenticated agreement routes. SDK methods that read one resource unwrap data and return the resource directly. List methods return the list envelope so your integration can read data, pageInfo, and meta.
const agreement = await client.getAgreement(agreementId);
const page = await client.listAgreements({ limit: 25 });
console.log(page.data, page.pageInfo.nextCursor, page.meta.requestId);
| Method | Use |
|---|
getOpenApiDocument() | Fetch GET /v0/openapi.json. |
getHealth() | Check GET /v0/health. |
listAgreements(params) | List paged agreement summaries visible to the API key. Supports limit, cursor, sort, chainId, state, and created/updated date filters. |
getAgreement(agreementId) | Read one agreement record. |
validateTemplate(agreement) | Validate authored agreement JSON with POST /v0/agreements/validate-template. |
validateDeployment(body) | Preflight deployment context with POST /v0/agreements/validate. |
deployWithPermit(body) | Deploy with a prepared EIP-712 permit. |
getAgreementState(agreementId) | Read the current agreement state. |
listAgreementInputs(agreementId, params) | Read paged input history. Supports limit, cursor, sort, userId, inputId, status, and created/updated date filters. |
submitAgreementInput(agreementId, body) | Submit a signed input. |
exchangeJson(method, path, body?) | Inspect raw response metadata without throwing for HTTP errors. |
request<T>(method, path, body?, okStatus?) | Low-level JSON request that returns the raw JSON body and throws on unexpected status. |
List filters use qs-style bracket modifiers. For example, { createdAt: { gte: '...' }, sort: { createdAt: 'desc' } } serializes as createdAt[gte]=...&sort[createdAt]=desc.
Agreement lists support chainId, state, createdAt, updatedAt, limit, cursor, and one sort field from createdAt, updatedAt, or displayName. Input history supports userId, inputId, status, createdAt, updatedAt, limit, cursor, and one sort field from createdAt or updatedAt. Date filters support gt, gte, lt, and lte; limit must be between 1 and 100.
const agreementsPage = await client.listAgreements({
chainId: 59141,
state: 'AWAITING_PAYMENT',
createdAt: { gte: '2026-05-01T00:00:00.000Z' },
sort: { createdAt: 'desc' },
limit: 25,
});
const nextAgreementPage = agreementsPage.pageInfo.nextCursor
? await client.listAgreements({ cursor: agreementsPage.pageInfo.nextCursor, limit: 25 })
: null;
const inputsPage = await client.listAgreementInputs('agreement-123', {
userId: 'platform-user-id',
status: 'MINED',
updatedAt: { lt: '2026-06-01T00:00:00.000Z' },
sort: { updatedAt: 'asc' },
limit: 25,
});
Validate and deploy with helpers
Use deployment preflight before signing when initValues, participant mappings, or observers affect the deployment request.
const chainId = 59141;
const validation = await client.validateDeployment({
agreement,
chainId,
initValues,
participants,
observers,
});
console.log(validation.variables);
Then use the high-level helper for the normal sign-and-submit path.
import { deployAgreementWithPermit } from '@cns-labs/agreements-api-client';
const agreementRecord = await deployAgreementWithPermit({
client,
walletClient,
publicClient,
chainId,
agreement,
displayName: 'Consulting Agreement',
initValues: validation.variables,
participants,
observers,
});
Participant-derived values in validation.variables are the effective values the SDK signs for deployment. Keep the same participants array in deployAgreementWithPermit(...) so hosted agreement context records the participant mappings.
import { submitAgreementInputWithPermit } from '@cns-labs/agreements-api-client';
await submitAgreementInputWithPermit({
client,
agreementId: agreementRecord.id,
walletClient,
publicClient,
chainId: agreementRecord.chainId,
agreementContractAddress,
agreement,
inputId: 'partyASignature',
values,
});
publicClient must be connected to the target chain/RPC. Input submissions also require chainId from the deployed agreement record so the helper can fail before signing if the client is connected to the wrong chain. The signing helpers use publicClient to resolve the chain-specific AgreementFactory or agreement contract context and to read the current permit nonce before signing.
Control deadlines and low-level signing
import {
computeDefaultDeadlineSeconds,
signAgreementInputPermit,
signDeployWithPermit,
} from '@cns-labs/agreements-api-client';
DEFAULT_PERMIT_DEADLINE_SECONDS is 3600. computeDefaultDeadlineSeconds(offsetSeconds = 3600) returns the current Unix time plus the offset.
High-level deployAgreementWithPermit(...) and submitAgreementInputWithPermit(...) default the deadline. Low-level signDeployWithPermit(...) and signAgreementInputPermit(...) require an explicit deadline.
Use low-level signing helpers when your application needs to sign first and submit later, inspect the signature, or compose the request body itself. Use EIP-712 Signing Reference only when you need to construct the typed data directly or debug helper behavior.
Handle API errors
The client throws AgreementsApiError when the response status does not match the expected success status.
import { AgreementsApiError } from '@cns-labs/agreements-api-client';
try {
await client.listAgreements();
} catch (error) {
if (error instanceof AgreementsApiError) {
console.error(error.status, error.errorPayload?.error.message ?? error.bodyText);
}
throw error;
}
AgreementsApiError exposes status, bodyText, parsedBody, and errorPayload. errorPayload.error.code is stable for branching, and errorPayload.error.requestId is the value to share when you need support to trace a request.
Use raw exchange for diagnostics
Use exchangeJson() when you need response metadata or raw body text during debugging. It does not throw for HTTP error status codes.
const response = await client.exchangeJson('GET', '/v0/agreements');
console.log(response.status, response.ok, response.parsedBody);
exchangeJson() accepts only GET and POST. It returns status, ok, headers, bodyText, and parsedBody.
Use path helpers when composing raw calls
import { agreementsApiPaths } from '@cns-labs/agreements-api-client';
const path = agreementsApiPaths.agreementState('agreement-123');
Path helper names are openapiJson, health, agreements, agreementsValidate, agreementsValidateTemplate, agreementsDeployWithPermit, agreement, agreementState, agreementInputs, and agreementInput. ID path helpers encode IDs and use API_BASE_PATH, which is /v0.
Root exports
The package root exports:
ApiClient
AgreementsApiError
extractAgreementsApiErrorMessage
agreementsApiPaths
getExecutionInputIds
API_BASE_PATH
API_ENVIRONMENT_BASE_URLS
API_MAJOR_VERSION
DEFAULT_API_ENVIRONMENT
resolveApiBaseUrl
joinUrl
computeDefaultDeadlineSeconds
DEFAULT_PERMIT_DEADLINE_SECONDS
signDeployWithPermit
signAgreementInputPermit
deployAgreementWithPermit
submitAgreementInputWithPermit
Related pages