Twokey

Docs

Seal the terms. Pay once.

Agents propose payouts. A person seals the exact terms. Twokey pays once.

Scripted Sandbox Base Sepolia · testnet

Sample data. Airwallex sandbox for fiat. USDC on Base Sepolia testnet. Not production. This package is not published. Install it from the workspace with pnpm --filter @twokey/sdk or file:packages/agent-sdk.

  1. 01

    Propose

    A scripted proposer sets the amount, payee, and expiry. Nothing is sent.

    Scripted
  2. 02

    Seal

    A person seals those exact terms. In the demo that is Hold to approve.

    Hold to approve
  3. 03

    Execute

    One seal pays once on each rail.

    Airwallex · sandbox Base Sepolia · testnet
  4. 04

    Status

    Read the demo run. Phase is sealed, executing, or terminal.

    Sample

Quickstart

About five calls once the demo executor and env are set. TWOKEY_EXECUTOR_URL is the executor you can reach. TWOKEY_API_TOKEN is the bearer token your local executor is started with. Run pnpm demo:executor locally. The Railway executor has no public URL.

quickstart.ts
import { Twokey } from '@twokey/sdk';

const twokey = new Twokey({
  baseUrl: process.env.TWOKEY_EXECUTOR_URL ?? '',
  token: process.env.TWOKEY_API_TOKEN ?? '',
  webBaseUrl: process.env.TWOKEY_WEB_URL,
});

const proposal = twokey.propose({
  amount: '2.50',
  currency: 'USD',
  counterpartyFingerprint: 'fp_northbeam_studio_sample',
  expiresAt: '2026-12-31T18:00:00.000Z',
  evidenceIds: ['inv_nb_1042_sample'],
});

const hashed = twokey.hashTerms(proposal.terms);

// A person seals these exact terms (Hold to approve in the demo).
// hashTerms only hashes. It is not approval.

const fiat = await twokey.execute(hashed, {
  rail: 'airwallex',
  logicalPaymentKey: 'demo-pay-1',
});

const usdc = await twokey.execute(hashed, {
  rail: 'base-usdc',
  logicalPaymentKey: 'demo-pay-1',
});

const view = await twokey.status(process.env.TWOKEY_RUN_ID ?? '');

Seal

propose returns a Scripted sample proposal. hashTerms hashes those terms with JCS and SHA-256. It is not approval. A person seals the exact terms with Hold to approve in the demo.

seal.ts
const proposal = twokey.propose({ amount: '2.50', currency: 'USD', /* sample payee */ });
const hashed = twokey.hashTerms(proposal.terms);
// hashed.seal is a hash. It does not approve the payout.
Seal reference
MethodEndpointParamsReturns
proposeLocal. Nothing is sent.amount, currency, counterpartyFingerprint, expiresAt, evidenceIdsScripted proposal: label Scripted, sample true, and the terms
hashTermsLocal. JCS and SHA-256.termsseal, canonicalJson, and terms. Not a human seal

Execute

execute sends POST /v1/execute with Authorization: Bearer and { sealedIntent, logicalPaymentKey, rail }. rail is airwallex (sandbox) or base-usdc (Base Sepolia · testnet). The same seal, key, and rail returns reused: true and the same provider id. A drifted current throws DriftError from HTTP 409. Nothing is paid.

execute-airwallex.ts
await twokey.execute(hashed, {
  rail: 'airwallex',
  logicalPaymentKey: 'demo-pay-1',
});
Execute reference
MethodEndpointParamsReturns
executePOST /v1/executesealed seal and terms, rail, logicalPaymentKey, optional currentok, voided false, requestId, rail, providerId, reused, seal
overLimitPOST /v1/base/over-limit-attemptsealed seal and termsDoes not return a payment. Throws OverLimitError

Status

status(runId) calls GET /api/runs/:id on the web app (webBaseUrl), not the executor. The id is the /run/<id> from the demo. The body is id, phase, seal, payLocked, amount, terms, events. HTTP 404 is not_found.

status.ts
const view = await twokey.status(runId);
// view.phase: sealed | executing | terminal
Status reference
MethodEndpointParamsReturns
statusGET /api/runs/:id on the web apprunId. Requires webBaseUrl.id, phase, seal, payLocked, amount, terms, events

Errors

These are the errors the client throws. They are not production settlement states.

Errors
ErrorWhen
DriftErrorHTTP 409 void. Amount, payee, or expiry drift. No money moves.
OverLimitErrorOver-limit refusal from the Base attempt. Not a payment.
NetworkErrorRetryable. Transport failure or HTTP 5xx. Retry the same seal, key, and rail.
NotFoundErrorHTTP 404 not_found from status().