Docs
Agents propose payouts. A person seals the exact terms. Twokey pays once.
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.
A scripted proposer sets the amount, payee, and expiry. Nothing is sent.
ScriptedA person seals those exact terms. In the demo that is Hold to approve.
Hold to approveOne seal pays once on each rail.
Read the demo run. Phase is sealed, executing, or terminal.
SampleAbout 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.
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 ?? '');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.
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.| Method | Endpoint | Params | Returns |
|---|---|---|---|
propose | Local. Nothing is sent. | amount, currency, counterpartyFingerprint, expiresAt, evidenceIds | Scripted proposal: label Scripted, sample true, and the terms |
hashTerms | Local. JCS and SHA-256. | terms | seal, canonicalJson, and terms. Not a human seal |
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.
await twokey.execute(hashed, {
rail: 'airwallex',
logicalPaymentKey: 'demo-pay-1',
});await twokey.execute(hashed, {
rail: 'base-usdc',
logicalPaymentKey: 'demo-pay-1',
});| Method | Endpoint | Params | Returns |
|---|---|---|---|
execute | POST /v1/execute | sealed seal and terms, rail, logicalPaymentKey, optional current | ok, voided false, requestId, rail, providerId, reused, seal |
overLimit | POST /v1/base/over-limit-attempt | sealed seal and terms | Does not return a payment. Throws OverLimitError |
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.
const view = await twokey.status(runId);
// view.phase: sealed | executing | terminal| Method | Endpoint | Params | Returns |
|---|---|---|---|
status | GET /api/runs/:id on the web app | runId. Requires webBaseUrl. | id, phase, seal, payLocked, amount, terms, events |
These are the errors the client throws. They are not production settlement states.
| Error | When |
|---|---|
DriftError | HTTP 409 void. Amount, payee, or expiry drift. No money moves. |
OverLimitError | Over-limit refusal from the Base attempt. Not a payment. |
NetworkError | Retryable. Transport failure or HTTP 5xx. Retry the same seal, key, and rail. |
NotFoundError | HTTP 404 not_found from status(). |