Skip to content
out-layerPublic

About

TypeScript SDK for OutLayer Agent Custody

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

@outlayer/sdk

TypeScript SDK for OutLayer Agent Custody — multi-chain wallets for AI agents with TEE-enforced policy, multisig approvals, and gasless cross-chain transfers via NEAR Intents.

Docs · Examples · API spec · Source

Why this SDK

If you've used Steward.fi or similar AI-agent wallet infra, you know the shape: API key in, signed transactions out. OutLayer keeps that shape but moves the trust root:

  • Keys live in a TEE (Intel Trust Domain Extensions on Phala Cloud). The infrastructure operator cannot extract them.
  • Policy is enforced inside the TEE, before signing, against an encrypted policy stored on the NEAR blockchain.
  • Sovereign exit is available: a customer can permissionlessly recover their wallet's master key even if OutLayer shuts down. (Requires opting into the vault flow — see the vault docs.)
  • Multi-chain wallets (NEAR, Ethereum, Solana, Bitcoin) with deterministic address derivation and gasless cross-chain transfers via NEAR Intents.

Install

npm install @outlayer/sdk

Requires Node 18+ (uses native fetch and crypto.randomUUID). Works in the browser, Bun, and Deno with no extra config.

60-second quickstart

import { OutlayerClient } from '@outlayer/sdk';

// 1. Register an anonymous wallet. The API key is shown ONCE — save it.
const { apiKey, walletId, handoffUrl } = await OutlayerClient.register();
console.log('API key:', apiKey);
console.log('Set up policy:', handoffUrl);

// 2. Use the wallet
const client = new OutlayerClient({ apiKey });

const addr = await client.getAddress('near');
console.log('NEAR address:', addr.address);

const balance = await client.getBalance({ chain: 'near' });
console.log('Balance:', balance.balance);

// 3. Withdraw across chains — gasless via NEAR Intents
const result = await client.withdraw({
  chain: 'ethereum',
  to: '0x742d35Cc6634C0532925a3b844Bc9e7595f8b4f5',
  amount: '1000000', // 1 USDT (6 decimals)
  token: 'nep141:usdt.tether-token.near',
});

if (result.status === 'pending_approval') {
  console.log(`Awaiting ${result.required} approvals; ${result.approved} so far.`);
} else {
  console.log('Submitted as request', result.request_id);
}

That's the whole flow. The wallet has no policy yet, so withdraws are unrestricted; visit handoffUrl in a browser to set spending limits, allowlists, and multisig.

What's in the SDK

Surface What it does
OutlayerClient.register() Create a new wallet, get an API key
client.getAddress(chain, subPath?) Derive address for NEAR / EVM (incl. HyperEVM) / Solana; subPath = an EVM sub-key
client.getBalance({...}) Read on-chain or intents.near balance
client.listTokens() Catalog of swap-capable tokens
client.call({...}) Sign and broadcast a NEAR contract call
client.transfer({...}) Native chain transfer (currently NEAR-only at the chain layer)
client.withdraw({...}) Gasless cross-chain withdraw via NEAR Intents
client.withdrawDryRun({...}) Policy + balance check without execution
client.swap({...}) Cross-chain swap via 1Click
client.swapQuote({...}) Price preview without execution
client.intentsDeposit({...}) Move an on-NEAR FT into intents.near
client.createDepositIntent({...}) Cross-chain deposit: one-time 1Click address to fund from another chain
client.getDepositStatus(id) Poll a cross-chain deposit intent
client.confidential*({...}) Same operations against the private confidential shard — shield / unshield / withdraw / swap / transfer / cross-chain deposit / balance (see below)
client.signMessage({...}) NEP-413 or raw message signing (NEAR identity)
client.evmSignTypedData({...}) Sign EIP-712 v4 typed data with the EVM key — e.g. a Polymarket CLOB order
client.evmSignMessage({...}) Sign an EIP-191 personal_sign message with the EVM key
client.evmSignTransaction({...}) Sign a raw unsigned EVM tx (you assemble + broadcast; gated by evm_sign.raw_tx)
client.claimTrialKey() The wallet's trial: a nonce-0 payment key for connector calls in its first week
client.redeemSponsorCode(code) Redeem a sponsor code (spn_…): a subscription on the nonce-0 key, paid by the sponsor
client.getPaymentKey() The nonce-0 key derived again — send it as X-Payment-Key; nothing to store
client.getRequest(id) Status of an async operation
client.listRequests({...}) List recent operations
client.policy.* Policy lifecycle (encrypt → sign → store)
client.approvals.* Multisig approval workflow
client.audit.list({...}) Event history

Full reference: API spec.

Confidential Intents

Mirror of the wallet's NEAR Intents operations against the Defuse confidential shard (intents.far on a private NEAR shard with no public RPC). Same SDK shape, same wk_ API key, no extra signing on your side — just a different balance shard. Every action is async: it returns a request_id you poll with client.getRequest(id) until the status is terminal (success / failed / refunded).

Routes return 503 confidential_unavailable unless the deployment has confidential intents enabled — treat that as "not offered here", not a retryable error (it surfaces as an OutlayerError with code === 'confidential_unavailable').

// SHIELD — move a public intents balance into the confidential shard.
// Links your wallet on chain (a convenience hop, NOT a private operation).
const shield = await client.confidentialDeposit({
  token: 'nep141:wrap.near',
  amount: '10000000000000000000000', // 0.01 wNEAR
});
// Async: poll client.getRequest(shield.request_id) until status is success / failed / refunded.

// Cross-chain DEPOSIT — fund the confidential balance from Solana USDC.
// The private path: your NEAR wallet never touches the public side.
const intent = await client.confidentialDepositIntent({
  source_asset: 'nep141:sol-5ce3bf3a31af18be40ba30f721101b4341690186.omft.near',
  amount: '500000', // 0.5 USDC (6 decimals)
});
console.log('Send 0.5 USDC on Solana to:', intent.deposit_address);
// then poll client.getRequest(intent.intent_id) until it settles.
// NB: deposit-intent's poll key is `intent_id` (the action methods return `request_id`).

// Withdraw a confidential balance to a NEAR account as native NEAR
// (chain="near" runs a native_withdraw via 1Click).
await client.confidentialWithdraw({
  chain: 'near',
  to: 'zavodil.near',
  amount: '10000000000000000000000',
  token: 'nep141:wrap.near',
});

// Read your confidential balance(s). Pass a token for one; omit for the full list.
const one = await client.confidentialBalance({ token: 'nep141:wrap.near' });
if (!('balances' in one)) console.log('wNEAR (confidential):', one.balance);

const all = await client.confidentialBalance();
if ('balances' in all) {
  for (const b of all.balances) console.log(b.token, b.balance);
}

Privacy, in one line: SHIELD / UNSHIELD publicly link your wallet to a "moved into the shielded pool" event; cross-chain DEPOSIT / WITHDRAW keep your NEAR wallet invisible on chain. It is a shielded pool, not a mixer — the shard operator, auditors, and law enforcement with a warrant can still read confidential state. See the full agent integration guide for the mental model, threat model, and privacy recipes.

Documentation

Topic Read this if you want to…
Getting started Register, set a policy, do your first withdraw
Wallet operations Send / receive / swap / withdraw — full method reference
Policy management Configure spending limits, allowlists, time windows, multisig thresholds
Multisig approvals Wire up the NEP-413 approval flow
Error handling Handle PolicyDeniedError, WalletFrozenError, retries
Vaults (sovereign custody) Bind a wallet to a deployed customer vault
Migration from raw HTTP Move from fetch('/wallet/v1/...') to the SDK

Vault custody (advanced)

For production deployments that need sovereign exit guarantees, bind a wallet to a customer-owned vault. Vault deployment happens via the dashboard or outlayer vault init CLI (not the SDK — your NEAR keys never touch us). Once deployed, binding is one option:

const wallet = await OutlayerClient.register({ vaultId: 'vault.alice.near' });

See docs/vaults.md and https://app.outlayer.ai/docs/vaults.

Examples

Runnable scripts in examples/:

  • 01-register.ts — register a wallet, derive addresses on 4 chains, check balance
  • 02-withdraw.ts — gasless cross-chain withdraw with dry-run + polling
  • 03-multisig.ts — submit a withdraw that triggers the approval flow
  • 04-agent-loop.ts — minimal autonomous agent that respects policy
  • 05-cross-chain-app.ts — end-to-end DeFi flow: cross-chain login pattern, deposit instructions, swap USDT → NEAR, stake with a validator, gasless withdraw back to Ethereum. CLI with sub-commands (addresses | balances | buy-near | stake | unstake | withdraw-eth | login-demo).
  • 06-confidential-roundtrip.ts — confidential shard round-trip: SHIELD → read balance → UNSHIELD, with 503 confidential_unavailable handling.
  • 07-inbox.ts — the owner's inbox from a server: sign a device in with the custody wallet, read the tasks the owner's agents left, close the notices with Got it.

Run with:

npx tsx examples/01-register.ts                                            # no auth needed
OUTLAYER_API_KEY=wk_... npx tsx examples/02-withdraw.ts                    # needs API key
OUTLAYER_API_KEY=wk_... npx tsx examples/05-cross-chain-app.ts addresses   # cross-chain identity

The owner's inbox

inbox reads the tasks an agent left for its owner — confirmations, inputs and notices — on a device of the owner's, and closes the notices with Got it. Every task is encrypted to the owner's devices; a device is a P-256 key pair the owner's wallet signs in with one NEP-413 statement. The module holds no wallet key: each signature is asked of a Signer, which for an OutLayer custody wallet is its sign-message (inbox.walletSigner(client)). It does not approve tasks: a task that asks something is approved where it is shown first.

import { OutlayerClient, inbox } from '@outlayer/sdk';

const signer = inbox.walletSigner(new OutlayerClient({ apiKey, network: 'testnet' }));
const device = await inbox.newDevice();            // keep inbox.exportDevice(device) server-side
const session = await inbox.signIn(
  { baseUrl: 'https://testnet-api.outlayer.ai', recipient: 'outlayer.testnet' },
  signer, accountId, device,                       // a day, by default; 30 days at most
);
for (const listed of (await inbox.listTasks(session)).tasks) {
  const task = await inbox.readTask(device, listed, accountId);
  if (task.envelope.kind === 'notice') await inbox.acknowledge(session, listed.id);
}

To be told of new tasks at a URL instead of asking: inbox.nameWebhook(session, url, signer) answers the secret, once; check each delivery with inbox.verifyWebhook(rawBody, request.headers.get('x-webhook-signature'), secret). One URL per owner — naming one replaces any other. A delivery says who asked whom, of what kind and when, and nothing of what the task shows: read the task to show it. A custody account never sent NEAR signs in too; a wallet whose policy lists the recipients of sign_message must list the OutLayer contract.

Errors

Every non-2xx response is thrown as a typed subclass of OutlayerError:

import { OutlayerClient, PolicyDeniedError, WalletFrozenError } from '@outlayer/sdk';

try {
  await client.withdraw({ chain: 'near', to: 'bob.near', amount: '100' });
} catch (err) {
  if (err instanceof PolicyDeniedError) {
    console.log('Policy rejected:', err.message);
  } else if (err instanceof WalletFrozenError) {
    console.log('Wallet is frozen by the controller');
  } else {
    throw err;
  }
}

See errors.md for the full list.

Retry, idempotency, network failures

Transient 5xx and network errors are retried automatically (3 attempts, exponential backoff 100ms → 1.6s). 4xx is not retried — those are deterministic.

Write operations get an auto-generated X-Idempotency-Key per call; retries from the SDK's own retry layer reuse the same key, so repeated calls don't double-spend. A key the server has seen answers duplicate_idempotency_key, thrown as a DuplicateRequestError carrying the request it belongs to (requestId, requestStatus, pollUrl; for a payment-check create, checks with each check_key, null when asked with another API key than the one that created it). This is what the SDK's own retry throws when the first attempt did run and the retry meets its key: catch it, take requestId, and poll pollUrl until the status is terminal — nothing ran twice. Pass answerWithinSeconds to be answered processing within that many seconds of the request's arrival instead of waiting the server's full wait (up to 80 s). To control idempotency yourself (e.g., for at-least-once delivery from a queue):

await client.withdraw({
  chain: 'near',
  to: 'bob.near',
  amount: '1000000000000000000000000',
  idempotencyKey: 'my-job-id-12345',
});

Reusing the same key does not re-execute, and does not return the stored result either: the call resolves with { error: 'duplicate_idempotency_key', message: 'Request already processed: <request_id>' } — a pointer. Read the outcome with getRequest(request_id).

Configuration

const client = new OutlayerClient({
  apiKey: process.env.OUTLAYER_API_KEY!,
  network: 'mainnet',                           // default; or 'testnet'
  baseUrl: 'https://api.outlayer.ai', // optional, overrides network
  fetch: customFetch,                           // optional, for SSR/proxies
  retry: {
    maxAttempts: 5,
    initialDelayMs: 200,
    maxDelayMs: 4000,
  },
});

Testnet vs mainnet

network: 'testnet' targets https://testnet-api.outlayer.ai. Useful for development without spending real funds.

Important: NEAR Intents (cross-chain swaps and gasless withdrawals) only work on mainnet. On testnet you can still:

  • register a wallet, derive addresses
  • read balances, set policy, sign messages
  • submit NEAR contract calls
  • sign EVM payloads (evmSignTypedData / evmSignMessage / evmSignTransaction) — pure off-chain secp256k1 signing, no intents/mainnet dependency

…but swap, intentsWithdraw, and intentsDeposit will fail at the intents layer.

Browser usage

Don't ship your API key to a browser. The key has full wallet authority. Either:

  1. Proxy through your backend — frontend calls your backend, your backend calls OutLayer.
  2. Use a payment key for that single tx — created via the dashboard, scoped to a specific operation.

The SDK works in browsers, but only with keys you've already gated.

Versioning

This is 0.1.0-alpha — expect breaking changes until 0.x stabilizes around v1. Breaking changes will be documented in CHANGELOG.md. Pin to a patch version (0.1.0-alpha.1) in production until v1.

Contributing

The OpenAPI spec is the source of truth — when adding endpoints, update out-layer/api-spec first, regenerate types here with npm run gen, then add ergonomic wrappers in src/client.ts.

git clone https://github.com/out-layer/sdk-js
cd sdk-js
npm install
npm run gen        # regenerate src/types.ts from spec
npm run typecheck
npm test
npm run build

License

MIT.

About

TypeScript SDK for OutLayer Agent Custody

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages