Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.shodai.network/llms.txt

Use this file to discover all available pages before exploring further.

Operate a deployed agreement by reading its current state, choosing a valid authored input, signing that input, submitting it, and then rereading state and input history. For TypeScript integrations, use the SDK operation loop first. Endpoint paths later in this page use full API paths under /v0 for raw HTTP and reference context.
Input submission requires a walletClient that can sign with the submitting account and a publicClient connected to the agreement chain. The signing wallet must be allowed by the authored input issuer; otherwise the signed input may be well-formed but invalid for the agreement lifecycle. For automated tests that only need signatures, see Create a test-only wallet client.
This page shows the API-assisted operation path. The same authorization model is grounded in EIP-712 signed inputs and the onchain execution engine.Applications that need direct onchain operation should refer to the EIP-712 Signing Reference and onchain architecture notes for typed data, contract addresses, and deployment details.

SDK operation loop

1

Read the agreement and current state

const agreementRecord = await client.getAgreement(agreementId);
const current = await client.getAgreementState(agreementId);
Use the hosted agreement record for address, chain, participant, observer, and stored agreement JSON context. Use current state to decide which authored inputs are candidates next.
2

Choose and sign a valid input

import { submitAgreementInputWithPermit } from '@cns-labs/agreements-api-client';
import type { AgreementJson } from '@cns-labs/agreements-protocol-evm';

const inputRecord = await submitAgreementInputWithPermit({
  client,
  agreementId,
  walletClient,
  publicClient,
  agreementContractAddress: agreementRecord.address!,
  agreement: agreementRecord.json as AgreementJson,
  inputId: 'submitInvoice',
  values,
});
Confirm that the input exists, the current state accepts it, the values match the input schema, and the signing wallet is allowed by the input issuer before submitting.
3

Reread state and history

const next = await client.getAgreementState(agreementId);
const inputs = await client.listAgreementInputs(agreementId);
Use state for the current lifecycle position and input history for audit and chronology.
publicClient must be connected to the target chain/RPC. Low-level signAgreementInputPermit(...) requires an explicit deadline; high-level submitAgreementInputWithPermit(...) defaults to computeDefaultDeadlineSeconds().

Raw operation loop

1

Read the agreement record

Use GET /v0/agreements/{id} when your product needs the hosted agreement record and context around it.
{
  "id": "agr_123",
  "status": "Deployed",
  "address": "0x3333333333333333333333333333333333333333",
  "displayName": "Advisory Retainer",
  "participants": [
    {
      "variableKey": "serviceProviderRepresentative",
      "walletAddress": "0x1111111111111111111111111111111111111111"
    },
    {
      "variableKey": "clientRepresentative",
      "walletAddress": "0x2222222222222222222222222222222222222222",
      "email": "client@example.com"
    }
  ],
  "observers": [
    "legal@example.com",
    "ops@example.com"
  ]
}
2

Read current state

Use GET /v0/agreements/{id}/state to decide which authored inputs are candidates next.
{
  "status": "Deployed",
  "state": "WORK_IN_PROGRESS"
}
Interpret the state against the authored agreement JSON.
3
4

Choose a valid authored input

Confirm that the input exists, the current state accepts it, the values match the input schema, and the signer is allowed by the input issuer.
{
  "inputs": {
    "submitInvoice": {
      "type": "VerifiedCredentialEIP712",
      "schema": "verified-credential-eip712.schema.json",
      "displayName": "Submit Invoice",
      "data": {
        "retainerBalanceBeforeInvoice": {
          "type": "uint256",
          "name": "Retainer Balance Before Invoice"
        },
        "invoiceLineItems": {
          "type": "string",
          "subtype": "invoice-csv",
          "name": "Invoice Line Items"
        },
        "submitInvoiceComment": "${variables.submitInvoiceComment}"
      },
      "issuer": "${variables.serviceProviderRepresentative.value}"
    }
  },
  "transitions": [
    {
      "from": "WORK_IN_PROGRESS",
      "to": "INVOICE_SUBMITTED",
      "conditions": [
        {
          "type": "isValid",
          "input": "submitInvoice"
        }
      ]
    }
  ]
}
5

Sign and submit the input

Submit the signed input to POST /v0/agreements/{id}/input.
{
  "inputId": "submitInvoice",
  "values": {
    "retainerBalanceBeforeInvoice": 1000,
    "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000",
    "submitInvoiceComment": "Invoice submitted for April services."
  },
  "signer": "0x1111111111111111111111111111111111111111",
  "deadline": 1776219513,
  "signature": {
    "v": 27,
    "r": "0x...",
    "s": "0x..."
  }
}
The TypeScript client uses a one-hour default permit lifetime through computeDefaultDeadlineSeconds(). Use a shorter deadline if your integration requires a tighter replay window, and regenerate the signature whenever the deadline expires.
6

Reread state and history

After a successful submission, call GET /v0/agreements/{id}/state and GET /v0/agreements/{id}/inputs to confirm where the agreement landed and what was recorded.

Understand the input record

When POST /v0/agreements/{id}/input succeeds, the API returns an input record.
{
  "agreementAddress": "0x3333333333333333333333333333333333333333",
  "chainId": 59141,
  "inputId": "submitInvoice",
  "values": {
    "retainerBalanceBeforeInvoice": 1000,
    "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000",
    "submitInvoiceComment": "Invoice submitted for April services."
  },
  "txHash": "0x4444444444444444444444444444444444444444444444444444444444444444",
  "payload": "0x...",
  "status": "MINED",
  "blockNumber": 123456
}
The input record proves the event was accepted and recorded. It does not replace reading current state when your product needs to know where the lifecycle is now.

Read input history

Use GET /v0/agreements/{id}/inputs to inspect recorded submissions. Add ?userId=<id> when you need the optional platform user filter.
[
  {
    "inputId": "submitInitialPaymentProof",
    "values": {
      "awaitingPaymentPaymentLink": "https://example.com/tx/0xaaa",
      "awaitingPaymentComment": "Initial payment completed."
    },
    "txHash": "0x1111111111111111111111111111111111111111111111111111111111111111",
    "status": "MINED",
    "blockNumber": 123450
  },
  {
    "inputId": "submitInvoice",
    "values": {
      "retainerBalanceBeforeInvoice": 1000,
      "invoiceLineItems": "2026-04-01,Advisory services,10,100,1000",
      "submitInvoiceComment": "Invoice submitted for April services."
    },
    "txHash": "0x4444444444444444444444444444444444444444444444444444444444444444",
    "status": "MINED",
    "blockNumber": 123456
  }
]

Handle state synchronization

You may see an input record with status: "MINED" before the state view reflects the new lifecycle position. Reread state when your product needs certainty about the current position, and use input history for audit and chronology.

Helper boundaries

submitAgreementInputWithPermit(...) signs the input payload and calls client.submitAgreementInput(...) with the resulting signer, deadline, and signature. Use signAgreementInputPermit(...) plus client.submitAgreementInput(...) when your application needs to sign first and submit later.

Why a submission may not move the agreement

When a submission does not advance the agreement, check:
  1. the agreement is in the state that accepts the input
  2. the submitted inputId exists in the authored agreement
  3. the submitted values match the input schema
  4. the signer is allowed by the input issuer
  5. the transition condition references that input from the current state
  6. the signature has not expired and was generated for this exact payload