@kagamidigital/salt-sdk-mirror
    Preparing search index...

    Class Salt

    The Salt SDK

    Index

    Constructors

    • Creates an instance of the Salt SDK

      Parameters

      • Optionalparams: {
            authToken?: string;
            domain?: string;
            environment: Environment;
            manualReconnectIntervalMs?: number;
            refreshToken?: string;
        }

        The constructor parameters

        • OptionalauthToken?: string

          The authentication token for the Salt SDK. See Salt.authenticate for the full authentication flow, or Salt#setAuthToken to set a pre-existing token after calling the constructor.

          This is the short-lived access token: it expires 20 minutes after the server issues it, and on its own it cannot be renewed. An instance constructed with only an authToken therefore stops working at that point, in one of two ways — API calls reject with a 401 ApiError, while ceremonies (transaction signing, message signing, account creation) reject with a ValidationError reading No refresh token is available. Call authenticate() again.

          To get silent auto-refresh instead, pass ConstructorParams.refreshToken alongside it — obtained from Salt.getRefreshToken after Salt.authenticate.

          null
          
        • Optionaldomain?: string

          The domain this instance authenticates from, used as the SIWE domain when Salt.authenticate is called. It is carried inside the signed SIWE message; the backend resolves the tenant from it and verifies it against the tenant's registered allowed domains. Required to call Salt.authenticate (browser or not); clients constructed with a pre-existing authToken don't need it.

        • environment: Environment

          Environment to use. This will be optional in the future, but right now it is required. Use 'STAGING'

        • OptionalmanualReconnectIntervalMs?: number

          Period (ms) at which a fallback timer will manually reconnect a dead socket — recovers from socket.io's "fatal" disconnects (most notably io server disconnect, fired during relay restarts and deploys). Recommended for long-running daemons (e.g. 60_000 for one check per minute). Leave undefined to disable.

        • OptionalrefreshToken?: string

          A refresh token previously obtained from Salt.authenticate (or Salt.getRefreshToken). When provided, the SDK automatically refreshes an expired access token on a 401 and retries the request — no manual re-authentication needed.

          null
          

      Returns Salt

      InvalidParams thrown if the parameters passed in to the constructor are invalid. Params are optional - the default environment is TESTNET.

      InvalidEnvironment Thrown if the provided Environment is invalid.

      InvalidAuthToken Thrown if the provided auth token is invalid. The token must be a string. If you need to authenticate, simply omit ConstructorParams.authToken, then use authenticate.

      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET' });

      // Log in to an existing account
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({
      account,
      chain: arbitrumSepolia,
      transport: http(),
      });
      await salt.authenticate(walletClient);

      // You are ready to use the SDK. For example...
      const orgs = await salt.getOrganisations();
      import { Salt } from 'salt-sdk';

      const salt = new Salt({ environment: 'MAINNET' });
      import { Salt } from 'salt-sdk';

      // Connect to local development instance
      const salt = new Salt({ environment: {
      chainId: 421614,
      websocketUrl: 'https://localhost:8545',
      saltDomain: 'localhost',
      apiUrl: 'https://localhost:8545/api'
      }})
      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'STAGING',
      authToken: 'your-auth-token'
      });
      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'STAGING',
      robo: { apiKey: process.env.ROBO_API_KEY! },
      });

    Accessors

    • get userPublicKey(): string | undefined

      The authenticated user's secp256k1 public key, recovered server-side from the SIWE signature during authenticate. Distinct from the wallet's EVM address (a hash of this key) — needed wherever a caller must encrypt something to the user, e.g. the publicKey param of RoboHost.generateSetupScript and RoboHost.generateCloudFormationUrl. Undefined until authenticate completes, and cleared again on logout or an unrecoverable token refresh.

      Returns string | undefined

    Methods

    • Disconnect the websocket connection. Call this when you are done using a long-lived socket (e.g. after stopping a NudgeListener) to allow the Node.js process to exit cleanly.

      Returns void

      Self-contained methods like createAccount and submitTx disconnect automatically — you need this for NudgeListener

      const nudgeListener = await salt.listenToAccountNudges({ signer });
      // ... handle nudges ...
      nudgeListener.disableNudgeListener();
      salt.disconnect();
    • Subscribe to the session becoming unauthenticated — an explicit logout, or a refresh that could not recover it (the refresh token is missing, expired, or was revoked). Fires even when nothing in your own code is currently awaiting a rejection (e.g. a background socket reconnect that failed to refresh).

      This doesn't force any particular response — it's just the signal that the session is done, so you can chain whatever makes sense for your app: re-authenticate immediately, redirect to a login screen, log and exit, etc.

      Parameters

      • handler: () => void

      Returns () => void

      An unsubscribe function.

      salt.subscribeToAuthStateUnset(async () => {
      await salt.authenticate(walletClient);
      });
      const unsub = salt.subscribeToAuthStateUnset(() => {
      logger.error('session unrecoverable, exiting for restart');
      process.exit(1);
      });
    • Subscribe directly to the relay's nudge event on the underlying socket. This is the same wire-level event NudgeListener subscribes to internally; exposing it here lets consumers observe nudges without going through NudgeListener's validation, dedup, and nudgeReceived event chain — useful for diagnostics, custom routing, or applications that don't want the auto-join machinery.

      Must be called after authenticate (which initialises the socket).

      Parameters

      Returns () => void

      An unsubscribe function.

      const unsubscribe = salt.subscribeToNudgeEvent((nudge) => {
      console.log('nudge received for account:', nudge.accountId);
      });
      // later, to stop observing:
      unsubscribe();
    • Subscribe to the underlying socket's lifecycle: connect, disconnect, reconnect attempts, reconnect-failed terminal state, and auth errors from the relay. Useful for long-running consumers (notably robo daemons) that need to log, alert, or trigger re-auth on relay availability changes.

      Must be called after authenticate (which initialises the socket).

      The handler does not replay the current connection state on subscribe — it only fires on subsequent transitions. Seed your initial state from a separate check; do not assume the first event reflects the state at subscribe time.

      Parameters

      Returns () => void

      An unsubscribe function.

      const unsub = salt.subscribeToSocketConnectionState((state) => {
      if (state.type === 'reconnect_failed') {
      // relay gave up — re-auth or shut down
      }
      });
    • Subscribe to any application-level event pushed to this user by the relay (e.g. server-side status updates forwarded via POST /user). No-op before connect or authenticate has been called.

      The event name and payload type T are the caller's responsibility — salt-sdk does not define or validate application-specific event shapes.

      Type Parameters

      • T

      Parameters

      • event: string
      • handler: (data: T) => void

      Returns () => void

      An unsubscribe function.

      const unsub = salt.subscribeToUserEvent<{ online: boolean }>(
      'roboHostConnected',
      ({ online }) => console.log('robo online:', online)
      );
      // later:
      unsub();

    Accounts

    • Creates a new account within an Organisation. Registers the account, nudges co-signers to join the MPC keygen ceremony, and returns before the ceremony completes.

      Returns an AccountCeremony. Call wait() to drive the keygen ceremony and finalise the account (derives the public key and EVM address). If you skip wait().

      The SDK automatically determines the required robo signers based on the number of human signers provided: an account is always N humans + (N-1) robos, so signers must contain exactly 2, 3, or 4 human addresses (yielding 2+1, 3+2, or 4+3 signer configurations).

      Account creation is off-chain for the human and robo signers — no gas is required from any of them. The on-chain shard-registry write is performed and paid for server-side.

      The organisation must have a provisioned, online robo host (see createRoboHost). The SDK resolves the robo signers via the API and fails with RoboStatusError if too few robo signers are online.

      Parameters

      Returns Promise<HostAccountCeremony>

      An AccountCeremony with the account ID and a wait() method

      InvalidAuthToken if the authentication token is invalid

      InvalidSigner if signer is missing or has no attached account

      WrongChain if signer is on a chain other than the orchestration chain

      ValidationError if the caller is not included in signers, the number of human signers is not 2, 3, or 4, or the organisation has fewer registered robo guardians than the configuration requires

      RoboStatusError if the organisation has no online robo host

      SocketConnectError if the signer fails to connect to websockets, which are required for setup orchestration

      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const signer = createWalletClient({
      account,
      chain: arbitrumSepolia,
      transport: http(process.env.RPC_URL),
      });

      const ceremony = await salt.createAccount({
      name: 'Treasury',
      organisationId: 'org-id',
      signers: [
      '0x1111111111111111111111111111111111111111',
      '0x2222222222222222222222222222222222222222',
      ],
      signer,
      });

      const { account: created } = await ceremony.wait();
      console.log('Account created:', created.publicKey);
    • Gets the details of a specific account. The user must have adequate permissions to view the account.

      Parameters

      • accountId: string

        The ID of the account

      Returns Promise<SaltAccount>

      The account details

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the account does not exist or the API returns an error

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const account = await salt.getAccount('account-id');
      console.log(`Account: ${account.name} ID: ${account.id} Public key: ${account.publicKey}`);
    • Lists an account's signers in signer order, each tagged with its role (human or robo). Robo identity is not encoded in the signer list itself, so this resolves it from the organisation's robo set.

      Use this to label a "who's joining" presence list: the AccountCeremony presence/ready events carry only address and isOnline, so build a lookup from this method and join by address. Reading the robo set requires permission only human users hold (a robo guardian does not), which is why presence stays role-free.

      Parameters

      • accountId: string

        The ID of the account

      Returns Promise<AccountSigner[]>

      The account's signers, each with address and isRobo

      InvalidAuthToken if the authentication token is invalid.

      ApiError if the account does not exist or the API returns an error

      const ceremony = await salt.createAccount({ ... });
      const signers = await salt.getAccountSigners(ceremony.accountId);
      const isRobo = new Map(
      signers.map((s) => [s.address.toLowerCase(), s.isRobo])
      );

      ceremony.on('presence', ({ signers }) => {
      signers.forEach((s) =>
      console.log(s.address, isRobo.get(s.address.toLowerCase()), s.isOnline)
      );
      });
    • Gets the transactions of a specific account. The user must have adequate permissions to view the account's transactions.

      Parameters

      • accountId: string

        The ID of the account

      Returns Promise<AccountTransaction[]>

      A list of transactions for the account

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the account does not exist or the API returns an error

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const transactions = await salt.getAccountTransactions('account-id');
    • Join an existing keygen ceremony from a received nudge payload. Use this when the nudge was received and stored before the signer was available (e.g. before the wallet was connected), so the join() closure can be constructed lazily at the point the user consents.

      Parameters

      • nudge: ValidKeygenNudgePayload

        Raw keygen nudge payload as received from the relay.

      • signer: ViemWalletClientLike

        The co-signer's viem WalletClient.

      • Optionaloptions: { resolveAccount?: boolean }

        Optional behaviour flags. resolveAccount fetches the account record during the ceremony, enabling address-level huddle presence (presence/ready/onlineSigners) and returning the finalized account from AccountCeremony.wait. Off by default — the MPC protocol needs none of it; opt in only when rendering presence or you need the account metadata. Always forced off for robo clients, which may not be permitted to read the record.

      Returns Promise<AccountCeremony>

      An AccountCeremony ready for AccountCeremony.wait.

      Call this at most once per nudge session. Unlike the NudgeListener join paths (which dedupe by session ID and refuse a second join), this manual path does not guard against a double-join: joining the same session twice starts a duplicate keygen session — a duplicate partyId broadcasting conflicting round-1 commitments — which corrupts peers with CBOR-decode / DLog errors. Callers must guard against double-fires (e.g. a React double-render or a retried click).

      InvalidAuthToken if the authentication token is invalid.

      InvalidSigner if signer has no attached account.

      SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration

      // `nudge` was received and stored earlier (e.g. via a NudgeListener),
      // before the wallet was connected. Join once `signer` is available and
      // the user has consented.
      const ceremony = await salt.joinAccountCeremony(nudge, signer);
      await ceremony.wait();
    • Opens a websocket connection to receive account setup nudges and automatically joins the MPC keygen ceremony for each one.

      The caller's signer is used to save the keyshare once the ceremony completes. Listen to the setupComplete event on the returned NudgeListener to be notified when a new account has been set up.

      Parameters

      • params: { autoJoin?: boolean; resolveAccount?: boolean; signer: ViemWalletClientLike }

        signer: the viem WalletClient that will participate in keygen ceremonies

        • OptionalautoJoin?: boolean

          Whether the listener should automatically join every incoming keygen ceremony. Defaults to true. Set to false to receive nudgeReceived events instead and decide per-nudge whether to call the supplied join callback.

        • OptionalresolveAccount?: boolean

          Fetch the account record for each joined ceremony, enabling address-level huddle presence and the account returned from the ceremony's wait(). Off by default — opt in only when rendering presence or you need the account metadata. Always forced off for robo clients. See joinAccountCeremony.

        • signer: ViemWalletClientLike

      Returns Promise<NudgeListener>

      NudgeListener for lifecycle events

      InvalidAuthToken if the authentication token is invalid

      SocketError if the websocket connection fails

      InvalidSigner if signer has no attached account

      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const account = privateKeyToAccount(process.env.PRIVATE_KEY);
      const signer = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });

      const nudgeListener = await salt.listenToAccountNudges({ signer });

      nudgeListener.on('setupComplete', ({ account }) => {
      console.log('Joined account:', account.id);
      });

      // When done listening
      nudgeListener.disableNudgeListener();
      salt.disconnect();
    • Hosts an MPC threshold signing ceremony over a plain-text message, hashed per EIP-191 — the personal_sign scheme, where the message is prefixed with "\x19Ethereum Signed Message:\n<length>" before hashing. Use it for login challenges, off-chain proofs of account control, and anything else a wallet's personal_sign would cover. To sign EIP-712 structured data instead, use signTypedData.

      message is always treated as UTF-8 text: a 0x-prefixed string is hashed as those literal characters, never decoded to bytes. This method signs no other form — it is not the (deprecated, unprefixed) eth_sign, and it never accepts a pre-computed digest, so what it signs can always be reconstructed from the text you passed in.

      Mirrors submitTx: it returns the SignMessageHostCeremony rather than the signature, so you can subscribe to progress events before driving it. Call wait() to run the ceremony and obtain the assembled signature.

      All account signers must have an active NudgeListener so they can join the ceremony automatically. This method drives the host side; co-signers are nudged and handled transparently.

      Parameters

      • params: { accountId: string; message: string; signer: AttachedWalletClient }

        accountId: the account to sign for; signer: the viem WalletClient for the calling party (used to load their keyshare); message: the UTF-8 message to sign

      Returns Promise<SignMessageHostCeremony>

      A SignMessageHostCeremony. Await its wait() for the EvmSignature.

      InvalidAuthToken if the authentication token is invalid

      InvalidSigner if signer has no attached account

      WrongChain if signer is on the wrong orchestration chain

      InvalidMessage if message is empty

      ValidationError if the caller is not a signer on the account

      ApiError if the account does not exist or the API returns an error

      SaltCeremonyError if the signing ceremony fails

      RoboStatusError if the account's robo quorum is not met (thrown from wait())

      SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration

      import { verifyMessage } from 'viem';

      const ceremony = await salt.signPersonalMessage({
      accountId: account.id,
      signer,
      message: 'hello world',
      });
      const { signature } = await ceremony.wait();

      // The signature verifies against the account's external address —
      // `account.evmAddress`, not the address of the signer that hosted.
      const valid = await verifyMessage({
      address: account.evmAddress!,
      message: 'hello world',
      signature,
      });
      const ceremony = await salt.signPersonalMessage({
      accountId: account.id,
      signer,
      message: 'hello world',
      });

      // Subscribe before awaiting to watch the signing huddle assemble —
      // the host plus the account's robo guardians joining to co-sign.
      ceremony.on('presence', ({ joined, total, signers }) => {
      console.log(`${joined} of ${total} signers present`);
      signers.forEach((s) => console.log(s.address, s.isOnline));
      });

      const { signature } = await ceremony.wait();
      const { account } = await (await salt.createAccount({ ... })).wait();

      // All signers must have their NudgeListeners running before this call
      const ceremony = await salt.signPersonalMessage({
      accountId: account.id,
      signer,
      message: 'challenge',
      });
      await ceremony.wait();
      console.log('Keyshare verified — backup is intact');
    • Hosts an MPC threshold signing ceremony over EIP-712 typed structured data — the eth_signTypedData_v4 scheme, where the digest is built from the domain separator and the hashed struct rather than from free text. Use it wherever a contract or protocol expects a typed-data signature. For plain-text messages use signPersonalMessage.

      The caller's signer plus the account's robo guardians co-sign, producing a single EVM signature that recovers to the account's public key — the same human + robo path as signPersonalMessage and submitTx, differing only in how the signed digest is derived.

      As with signPersonalMessage, no pre-computed digest is ever accepted: the typed data is distributed to the co-signers, and each derives the digest itself, so what was signed is always reconstructible from the typedData you passed in.

      Parameters

      • params: { accountId: string; signer: AttachedWalletClient; typedData: SaltTypedData }

        accountId: the account to sign for; signer: the viem WalletClient for the calling party (used to load their keyshare); typedData: the SaltTypedData to sign

      Returns Promise<SignMessageHostCeremony>

      A SignMessageHostCeremony. Await its wait() for the EvmSignature.

      A typed-data signature is an off-chain authorisation and is not subject to the account's transaction policies — those are evaluated by submitTx against a transaction. Structures such as ERC-2612 Permit let a third party move funds once signed, so treat what you sign here with the same care as a transaction.

      InvalidAuthToken if the authentication token is invalid

      InvalidSigner if signer has no attached account

      WrongChain if signer is on the wrong orchestration chain

      InvalidTypedData if typedData is malformed, or primaryType names no struct in types

      Error (from viem, e.g. InvalidAddressError) if a field value in message does not match the type declared for it

      ValidationError if the caller is not a signer on the account

      ApiError if the account does not exist or the API returns an error

      SaltCeremonyError if the signing ceremony fails

      RoboStatusError if the account's robo quorum is not met (thrown from wait())

      SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration

      import { verifyTypedData } from 'viem';

      const typedData = {
      domain: {
      name: 'Salt Example',
      version: '1',
      chainId: 421614,
      verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
      },
      types: {
      Mail: [
      { name: 'from', type: 'address' },
      { name: 'to', type: 'address' },
      { name: 'contents', type: 'string' },
      ],
      },
      primaryType: 'Mail',
      message: {
      from: account.evmAddress!,
      to: '0x2222222222222222222222222222222222222222',
      contents: 'Hello from Salt',
      },
      } as const;

      const ceremony = await salt.signTypedData({
      accountId: account.id,
      signer,
      typedData,
      });
      const { signature } = await ceremony.wait();

      const valid = await verifyTypedData({
      address: account.evmAddress!,
      ...typedData,
      signature,
      });
      const ceremony = await salt.signTypedData({
      accountId: account.id,
      signer,
      typedData,
      });

      ceremony.on('stateChanged', ({ stage }) => console.log('now', stage));
      const { signature } = await ceremony.wait();
    • Verifies the integrity of all keyshares for a Salt account by exercising two independent signing flows against a fixed challenge:

      1. Recovery flow — nudges every non-robo (human) signer. Proves that the human keyshares alone can produce a valid threshold signature, validating the account's recovery path.

      2. Regular flow — nudges every robo signer (the calling human is the host). Proves that the standard transaction signing path — one human plus the robo guardians — is operational.

      Both ceremonies must complete successfully for the account's keyshares to be considered verified. A failure in either indicates a corrupted or missing keyshare backup.

      All account signers (human and robo) must have an active NudgeListener so they can join each ceremony automatically.

      Parameters

      • params: { accountId: string; signer: AttachedWalletClient }

        accountId: the account to verify; signer: the viem WalletClient for the calling party (must be a human signer on the account)

      Returns Promise<VerifyAccountResult>

      A VerifyAccountResult containing the confirmed account address and the signatures produced by each flow.

      InvalidAuthToken if the authentication token is invalid

      InvalidSigner if signer has no attached account

      WrongChain if signer is on the wrong orchestration chain

      SaltCeremonyError if either signing ceremony fails

      ValidationError if the caller is not a signer on the account

      ApiError if the account does not exist or the API returns an error

      RoboStatusError if the account's robo quorum is not met for the regular flow

      SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration

      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });

      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const signer = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });

      // All signers (human and robo) must have their NudgeListeners running
      const { accountAddress, signatures } = await salt.verifyAccount({
      accountId: 'account-id',
      signer,
      });
      console.log('Keyshares verified for', accountAddress);
      console.log('Recovery flow signer:', signatures.recovery.recoveredAddress);
      console.log('Regular flow signer:', signatures.regular.recoveredAddress);

    Authentication

    • Initiate the SIWE authentication flow to get an authentication token from the Salt API. Signs a SIWE message + nonce with the supplied viem WalletClient; the server verifies and returns an access token plus a companion refresh token.

      If you already have a token, set it via setAuthToken or in the constructor.

      Parameters

      Returns Promise<string>

      The short-lived access token, which expires 20 minutes after it is issued. Both it and the refresh token issued alongside it are stored on the client, together with the user's public key (see userPublicKey), so this instance auto-refreshes for as long as it lives. To survive a restart, read the refresh token with getRefreshToken and persist it too: an instance reconstructed from the access token alone cannot refresh. See ConstructorParams.authToken.

      InvalidSigner if walletClient has no attached account

      ValidationError if no domain was configured on the constructor

      InvalidUrl if the API URL or path is invalid

      ApiError if the authentication request fails

      Error (from the wallet, e.g. viem's UserRejectedRequestError) if the user rejects signing

      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET' });

      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({
      account,
      chain: arbitrumSepolia,
      transport: http(),
      });

      await salt.authenticate(walletClient);
      import { Salt } from 'salt-sdk';
      import { createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET' });

      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({
      account,
      chain: arbitrumSepolia,
      transport: http(),
      });
      const authToken = await salt.authenticate(walletClient);

      // Persist both: the access token expires after 20 minutes, and only the
      // refresh token can mint a new one. `secureStorage` is a placeholder for
      // your app's secure persistence — localStorage is not a safe choice here.
      const refreshToken = salt.getRefreshToken();
      await secureStorage.set('salt.authToken', authToken);
      if (refreshToken) await secureStorage.set('salt.refreshToken', refreshToken);

      // Next launch: hand back both, and the SDK refreshes on expiry by itself.
      // The refresh token is valid for 30 days, renewed on every use, so a
      // session resumes for as long as it is used at least that often.
      const resumed = new Salt({
      environment: 'TESTNET',
      authToken: await secureStorage.get('salt.authToken'),
      refreshToken: await secureStorage.get('salt.refreshToken'),
      });
    • Connect the underlying relay socket using the current auth token. Idempotent — safe to call repeatedly; a live connected socket is left untouched. Call this immediately after setAuthToken to ensure the socket is ready before any nudges or server-push events are emitted, without waiting for a ceremony entry point.

      Returns Promise<void>

      SocketError if the connection fails.

      const salt = new Salt({ environment: 'TESTNET' });
      salt.setAuthToken(savedAuthToken);
      await salt.connect(); // socket is live; nudges/user events now flow
    • Returns the current refresh token, if any. Persist this (e.g. to secure storage) and pass it back via ConstructorParams.refreshToken to resume a session after a restart without re-running the SIWE flow.

      Note the refresh token rotates on every use, so always read the latest value (e.g. after each call) rather than caching the first one.

      Returns string | null

      await salt.authenticate(walletClient);
      const refreshToken = salt.getRefreshToken();
      // `secureStorage` is a placeholder for your app's secure persistence
      if (refreshToken) await secureStorage.set('salt.refreshToken', refreshToken);
      // next launch: new Salt({ environment, refreshToken })
    • Logs out the current session: revokes the refresh token server-side, clears the in-memory tokens, and disconnects the websocket. After this the instance is unauthenticated — call authenticate to use it again.

      Resolves even if the network call fails; local state is always cleared.

      Returns Promise<void>

      await salt.logout();
      
    • Sets the authentication token for the Salt SDK. This is useful if you have previously logged in and stored the token. You can also provide the token to the constructor If you do not have a token already, you can use the authenticate method to obtain one.

      Parameters

      • authToken: string

        The authentication token to use for this connection

      Returns void

      This is equivalent to providing the token to the constructor, and carries the same limitation: an access token expires 20 minutes after it is issued and cannot be renewed on its own. This method sets only the access token, so a session restored with it alone cannot auto-refresh — see ConstructorParams.authToken for how that failure surfaces. To restore a refreshable session, pass both tokens to the constructor instead.

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      });

      // Retrieve an access token obtained earlier in this process. `secureStorage`
      // is a placeholder for your app's secure persistence.
      const authToken = await secureStorage.get('salt.authToken');

      salt.setAuthToken(authToken);

      // You are ready to use the SDK, until the token expires 20 minutes after
      // it was issued. To resume a session that outlives that, construct with a
      // refresh token as well — see `ConstructorParams.authToken`.
      await salt.getOrganisations();

    Organisations

    • Accepts an Invitation to an Organisation. The list of transactions for the current user can be retrieved with getOrganisationsInvitations.

      Parameters

      • invitationId: string

        The ID of the invitation to accept

      Returns Promise<{ organisation: Organisation }>

      InvalidAuthToken if the authentication token is invalid

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const invitations = await salt.getOrganisationsInvitations();
      for (const invitation of invitations) {
      console.log(`Accepting invitation to organisation ${invitation.organisation_id}`);
      await salt.acceptOrganisationInvitation(invitation.id);
      }
    • Creates a new Organisation. The authenticated user will be the owner. Collaborators can optionally be invited at creation time — they will receive invitations that can be accepted via acceptOrganisationInvitation.

      This operation is off chain, so it does not require any gas.

      Parameters

      Returns Promise<Organisation>

      The created organisation

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the API returns an error (e.g. duplicate collaborator addresses, invalid name)

      SyntaxError if the API response is not valid JSON

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const org = await salt.createOrganisation({
      name: 'My Organisation',
      owner: {
      name: 'Alice',
      address: '0x1234567890123456789012345678901234567890',
      role: 'CEO',
      },
      });
      console.log('Created organisation:', org.id);
      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const org = await salt.createOrganisation({
      name: 'My Organisation',
      owner: {
      name: 'Alice',
      address: '0x1234567890123456789012345678901234567890',
      role: 'CEO',
      },
      collaborators: [
      {
      name: 'Bob',
      address: '0x2345678901234567890123456789012345678901',
      role: 'CFO',
      accessLevel: 2,
      },
      ],
      });
    • Declines a pending Invitation to an Organisation. The counterpart to acceptOrganisationInvitation; pending invitations are listed with getOrganisationsInvitations.

      Parameters

      • invitationId: string

        The ID of the invitation to decline

      Returns Promise<{ organisation: Organisation }>

      InvalidAuthToken if the authentication token is invalid

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const invitations = await salt.getOrganisationsInvitations();
      for (const invitation of invitations) {
      await salt.declineOrganisationInvitation(invitation.id);
      }
    • Gets the list of Accounts that belong to a Organisation.

      Parameters

      Returns Promise<SaltAccount[]>

      The list of accounts

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the API returns an error

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const accounts = await salt.getAccounts('organisation-id');
      accounts.forEach(account => {
      console.log(`Account: ${account.name} ID: ${account.id} Public key: ${account.publicKey}`);
      });
    • Gets a single Organisation by its ID. The user must be a collaborator on the organisation.

      Parameters

      • organisationId: string

        The ID of the organisation to fetch

      Returns Promise<OrganisationByIdResponse>

      An object wrapping the organisation under an organisation key

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the organisation does not exist or the user lacks permission to view it

      SyntaxError if the API response is not valid JSON

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const { organisation } = await salt.getOrganisationById('org-id');
      console.log(organisation.name, organisation.collaborators);
    • Gets the list of Organisations that the current user is a collaborator on. Requires authentication.

      Returns Promise<Organisation[]>

      The list of organisations

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const organisations = await salt.getOrganisations();
      organisations.forEach(org => {
      console.group('Organisation:', org.name);
      org.collaborators.forEach(collaborator => {
      console.log('Collaborator:', collaborator.name);
      });
      console.groupEnd();
      });
    • Returns pending Invitation to an Organisation for the current user. They can be accepted using acceptOrganisationInvitation.

      Returns Promise<{ invitations: Invitation[] }>

      List of invitations that are pending for this user

      InvalidAuthToken if the authentication token is invalid

      SocketError if the socket connection could not be established

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const invitations = await salt.getOrganisationsInvitations();
      if (invitations.length > 0) {
      console.log('You have pending invitations');
      } else {
      console.log('No pending invitations');
      }
    • Invites a new collaborator into an existing Organisation. The invitee is added with status "Invited" and can join by accepting the invitation via acceptOrganisationInvitation. Only owners of the organisation may invite collaborators.

      Every existing collaborator is left untouched. To add collaborators at creation time instead, use the collaborators option of createOrganisation.

      Parameters

      • organisationId: string

        The ID of the organisation to invite into

      • invitee: InviteCollaboratorParams

        The invitee's address and role, plus optional name and accessLevel (defaults to 2, Member)

      Returns Promise<void>

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      InvalidParams if a collaborator with the same address is already in the organisation

      ApiError if the organisation does not exist or the user lacks permission to update it

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.inviteCollaborator('org-id', {
      name: 'Bob',
      address: '0x2345678901234567890123456789012345678901',
      role: 'CFO',
      accessLevel: 'member',
      });
    • Leaves an Organisation as the authenticated user. Use this to remove yourself; to remove another collaborator as an owner, use removeCollaborator.

      The collaborator is marked Inactive (the API retains collaborators for audit history) and any pending invitation for you is dropped. An organisation must always keep at least one active owner, so the last active owner cannot leave.

      Parameters

      • organisationId: string

        The ID of the organisation to leave

      Returns Promise<{ organisation: Organisation }>

      The updated organisation

      InvalidAuthToken if the authentication token is invalid

      ApiError if you are not a collaborator, or leaving would remove the last active owner

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.leaveOrganisation('org-id');
    • Removes a collaborator from an Organisation by revoking their access. The Salt API retains collaborators for audit history, so the collaborator is marked Inactive rather than deleted — they will still appear in getOrganisationById with status Inactive. Every other collaborator is left untouched. Only owners of the organisation may remove collaborators.

      An organisation must always retain at least one active owner, so removing the last active owner is rejected.

      Parameters

      • organisationId: string

        The ID of the organisation the collaborator belongs to

      • collaboratorAddress: string

        The Ethereum address of the collaborator to remove

      Returns Promise<void>

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      InvalidParams if the collaborator is not found, or removing them would leave no active owner

      ApiError if the organisation does not exist or the user lacks permission to update it

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.removeCollaborator(
      'org-id',
      '0x2345678901234567890123456789012345678901'
      );
    • Updates a single collaborator within an Organisation. Use this to change a collaborator's display name, their access level (the permission tier the API enforces), their free-text role label (cosmetic, no permission effect), or their status (to re-activate a removed collaborator or deactivate one). Only owners of the organisation may update collaborators.

      Every other collaborator is left untouched. An organisation must always retain at least one active owner, so demoting the last owner or deactivating them is rejected. A collaborator's address is immutable and cannot be changed here.

      Parameters

      • organisationId: string

        The ID of the organisation the collaborator belongs to

      • collaboratorAddress: string

        The Ethereum address of the collaborator to update

      • changes: UpdateCollaboratorParams

        The fields to change; provide any of name, role, accessLevel, status

      Returns Promise<void>

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      InvalidParams if no changes are given, the collaborator is not found, or the change would leave no active owner

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      // Collaborators are unique by EVM address, so a removed collaborator
      // cannot be re-invited — reactivate them instead.
      await salt.updateCollaborator(
      'org-id',
      '0x1234567890123456789012345678901234567890',
      { status: 'Active' }
      );

      ApiError if the organisation does not exist or the user lacks permission to update it

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.updateCollaborator(
      'org-id',
      '0x1234567890123456789012345678901234567890',
      { name: 'Alice', accessLevel: 'owner', role: 'CEO' }
      );

    Policies

    • Creates a new Policy on a Salt account. Policies control what transactions are allowed, denied, or require approval before being signed by the robo guardians.

      Only one policy may exist per type + chain on an account; attempting to create a second of the same type for the same chain throws a DuplicatePolicyError. To express several constraints of one type, put them in that policy's params array.

      nominated_approvers policies cannot be created: the policy type exists on the API but approval enforcement is not yet implemented, so creating one throws a ValidationError. For contract_param_restriction policies each restriction's solidityType is derived from its functionSignature/ paramIndex (so you need not supply it); this catches a few common mistakes early — malformed signatures, out-of-range indexes, or illegal operators — throwing a ValidationError before any request is made. It is not exhaustive validation.

      Parameters

      Returns Promise<Policy>

      The created policy

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ValidationError if the policy is a nominated_approvers policy (not yet implemented), or a contract_param_restriction policy with an invalid restriction

      DuplicatePolicyError if a policy of the same type already exists on the account for the same chain

      ApiError if the API returns an error (e.g. invalid policy type or params)

      SyntaxError if the API response is not valid JSON

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const policy = await salt.createAccountPolicy({
      accountId: 'account-id',
      type: 'allowed_recipients',
      chain: '11155111', // Sepolia
      params: {
      recipients: [
      { address: '0x1234567890123456789012345678901234567890', nickname: 'Treasury' },
      ],
      },
      });
      console.log('Created policy:', policy.id);
      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const policy = await salt.createAccountPolicy({
      accountId: 'account-id',
      type: 'transaction_limit_token_denominated',
      chain: '1', // Ethereum Mainnet
      params: {
      limits: [
      {
      address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
      amount: '1000',
      },
      ],
      },
      });
    • Deletes an existing Policy by its ID.

      Parameters

      • policyId: string

        The ID of the policy to delete

      Returns Promise<void>

      A promise that resolves when the policy has been deleted

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the policy cannot be found or the API returns an error

      updateAccountPolicy to change a policy's params instead of removing it

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.deleteAccountPolicy('policy-id');
    • Fetches an existing Policy by its ID.

      Parameters

      • policyId: string

        The ID of the policy to retrieve

      Returns Promise<Policy>

      The policy

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the policy cannot be found or the API returns an error

      SyntaxError if the API response is not valid JSON

      listAccountPolicies to fetch all of an account's policies when you don't have a policy ID

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const policy = await salt.getAccountPolicy('policy-id');
      console.log(`Policy type: ${policy.type} [chain ${policy.chain}]`);

      // PolicyParams is a union, so narrow it with an `in` check
      if ('recipients' in policy.params) {
      for (const recipient of policy.params.recipients) {
      console.log(` ${recipient.address} (${recipient.nickname ?? 'unnamed'})`);
      }
      }
    • Lists all policies attached to a Salt account.

      Reads the policies embedded in the account record, so this is a convenience wrapper over getAccount — use it when you only need the policy list.

      Parameters

      • accountId: string

        The ID of the account whose policies should be listed.

      Returns Promise<Policy[]>

      An array of policies attached to the account.

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the API returns an error.

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const policies = await salt.listAccountPolicies('account-id');
      console.log(`Account has ${policies.length} policies`);
      for (const policy of policies) {
      console.log(` ${policy.type} [${policy.chain}]: ${policy.id}`);
      }
    • Runs a policy check for a Salt account.

      Evaluates the account's policies that apply to the proposed transaction and returns which were validated, which were rejected (breach), and whether the account has policies that were not evaluated — see PolicyCheck. Run this before submitTx to surface policy violations early: if a breaching transaction is submitted anyway, the signing ceremony fails with a PolicyBreachError.

      Parameters

      Returns Promise<PolicyCheck>

      A PolicyCheck describing the outcome of each policy.

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the API request fails (e.g. the account does not exist or the caller lacks permission).

      const result = await salt.runPoliciesCheck('account-id', {
      nonce: 1,
      amount: '1000000000000000000', // 1 ETH, in wei
      from: '0xYourSaltAccountAddress', // the account sending the transaction, not you
      to: '0xRecipientAddress',
      network: '1',
      data: '0x',
      });

      if (result.policyBreach) {
      console.log('Rejected by:', result.rejectedPolicies.map(p => p.type));
      }
    • Updates an existing Policy by its ID. Replaces the policy's params with the new values provided.

      When updating a contract_param_restriction policy, each restriction's solidityType is derived from its functionSignature/paramIndex (so you need not supply it). This catches a few common mistakes early — a malformed signature, an out-of-range paramIndex, or an operator that is illegal for the parameter type — throwing a ValidationError before any request is made. It is not exhaustive validation.

      Parameters

      • policyId: string

        The ID of the policy to update

      • params: PolicyParams

        The new policy parameters

      Returns Promise<Policy>

      The updated policy

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ValidationError if updating a contract_param_restriction policy with an invalid or contradictory restriction

      ApiError if the API returns an error (e.g. policy not found or invalid params)

      SyntaxError if the API response is not valid JSON

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      const updated = await salt.updateAccountPolicy('policy-id', {
      recipients: [
      { address: '0x1234567890123456789012345678901234567890', nickname: 'Treasury' },
      { address: '0x2345678901234567890123456789012345678901', nickname: 'Payroll' },
      ],
      });
      console.log('Updated policy:', updated.id);
      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token',
      });

      await salt.updateAccountPolicy('policy-id', {
      limits: [
      { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', amount: '5000' },
      ],
      });

    Robos

    • Completes 2FA activation for a robo host, flipping it from inactive to active. This is what makes a robo host usable. getRoboQuorum.

      Authorised by the owner, not a privileged session. Like authenticate, this builds and signs a SIWE message itself — the caller passes the owner's walletClient and the host's setup OTP (the code shown during robo-guardian setup — see RoboHost.otp); this method embeds that OTP in the SIWE statement, has the wallet sign it, and submits it. That proves both control of the owner wallet and possession of the setup code. On success the server marks the robo active and consumes the OTP, so activation is single-use.

      Parameters

      Returns Promise<Robos>

      The updated robos record (with active: true)

      InvalidAuthToken if the authentication token is invalid

      ValidationError if called from a robo client, the signer is invalid, or the constructor was given no domain

      ApiError with status 403 if the signer is not the robo owner, 401 if the OTP is wrong, or 409 if the host is already active

      const host = await salt.getRoboHost({ organisationId: 'org-id' });
      const activated = await salt.activateRoboHost(
      { roboId: host!.id, otp: host!.otp! },
      ownerWalletClient
      );
      console.log(activated.active); // true
    • Registers robos for an Organisation and creates their host record. No robo is running after this call — it creates only the server-side record. The robo signers come online once you provision a host with RoboHost.generateSetupScript or RoboHost.generateCloudFormationUrl and its setup completes. Treat the setup OTP as a secret. Only organisation owners can create a robo host.

      Parameters

      Returns Promise<RoboHost>

      The created robo host, including its setup OTP

      This creates only the server-side record — its signers start empty and no robo is running or online as a result. The guardian signer addresses are registered separately during the provisioning flow (RoboHost.generateSetupScript / RoboHost.generateCloudFormationUrl), and a robo daemon must then SIWE-authenticate those signers into the /robo websocket namespace before they count as online.

      "Online" is tracked per signer address in the /robo namespace, not per organisation — so one running daemon's addresses satisfy any organisation whose robo record lists them. Re-pointing a daemon at another organisation's record is a configuration change, not a re-keying.

      Use getRoboQuorum to verify the organisation's robos are online before attempting createAccount — account creation fails with RoboStatusError if no robo host is reachable.

      InvalidAuthToken if the authentication token is invalid

      InsufficientPermissions if the caller is not an organisation owner

      ValidationError if called from a robo client

      ApiError with status 409 if the organisation already has a robo host

      ApiError if the API returns any other error

      const host = await salt.createRoboHost({
      name: 'My Org Robos',
      organisationId: 'org-id',
      ownerAddress: '0xowner',
      });
      const script = host.generateSetupScript({
      publicKey: ownerPublicKey,
      });
    • Fetches the existing robo host for an organisation — its public record only: name, signers, setup OTP (present until activation consumes it) and activation state. Everything needed to provision and activate a host is here; readable by any organisation collaborator with robos access, on any tenant.

      The host's encrypted seed backup is not included — fetch it separately with getRoboHostSecrets, which is restricted to owners on privileged sessions.

      Parameters

      • params: { organisationId: string }

        The ID of the organisation

      Returns Promise<RoboHost | null>

      The robo host or null if not found

      InvalidAuthToken if the authentication token is invalid

      ValidationError if called from a robo client

      ApiError if the robo host lookup fails

      getRoboHostSecrets for the encrypted seed backup

      const host = await salt.getRoboHost({ organisationId: 'org-id' });
      console.log(host?.signers);
    • Fetches a robo host's secret material — currently its encrypted wallet-seed backup. Restricted to organisation owners operating under privileged sessions (SIWE domain *.salt.space, or localhost against a dev backend); unlike getRoboHost, permission failures are not swallowed.

      Parameters

      • params: { roboId: string }

        The robo host id (see RoboHost.id)

      Returns Promise<RoboHostSecrets>

      The host's secrets — seed is null until provisioning uploads a backup

      InvalidAuthToken if the authentication token is invalid

      ValidationError if called from a robo client

      InsufficientPermissions if the caller is not an organisation owner, or the session is not privileged (unauthorized:not-privileged)

      ApiError if the robo host does not exist or the API errors

      const host = await salt.getRoboHost({ organisationId: 'org-id' });
      if (host?.provisioned) {
      const { seed } = await salt.getRoboHostSecrets({ roboId: host.id });
      }
    • Reports whether enough robos are online to run a ceremony of the given type (see RoboQuorumParams).

      • keygen — roster-scoped; the required count comes from humanCount.
      • signing / signMessage — account-scoped; only the account's enrolled robos count, and the required count is fixed by the account.

      Parameters

      Returns Promise<RoboQuorum>

      The robo quorum: { requiredCount, onlineCount, met }

      InvalidAuthToken if the authentication token is invalid

      ValidationError if a keygen humanCount is unsupported

      ApiError if the account or organisation does not exist, or the API returns an error

      const { met } = await salt.getRoboQuorum({
      sessionType: 'keygen',
      organisationId: 'org-id',
      humanCount: 2,
      });
      if (met) {
      // start the keygen ceremony
      }
      const { onlineCount, requiredCount, met } = await salt.getRoboQuorum({
      sessionType: 'signing',
      accountId: 'account-id',
      });
      if (!met) {
      console.warn(`Only ${onlineCount}/${requiredCount} of this account's robos online`);
      }
    • Reports the live presence of robos — an Organisation's whole roster, or the robos enrolled on a single account.

      • { organisationId } — every robo signer in the organisation's roster.
      • { accountId } — only the robos enrolled on that account (the ones holding its keyshares).

      Each signer carries an isOnline flag; the result also has an onlineCount and an isReachable liveness flag (see RoboStatus).

      Parameters

      • params: { organisationId: string } | { accountId: string }

        { organisationId } or { accountId }

      Returns Promise<RoboStatus>

      The robo signers' presence

      InvalidAuthToken if the authentication token is invalid

      ApiError if the API returns an error

      const { onlineCount, isReachable } =
      await salt.getRoboStatus({ organisationId: 'org-id' });
      console.log(`${onlineCount} robos online`, isReachable);

    Transactions

    • Gets the nonce for an account on a specific chain — the account's current on-chain transaction count (standard EVM nonce), fetched live from the destination network. submitTx uses the nonce, but if you do not specify one the SDK will automatically use this function to populate it.

      Parameters

      • accountId: string

        The ID of the account

      • chainId: number

        The ID of the chain

      Returns Promise<number>

      The nonce

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      ApiError if the account does not exist or the API returns an error

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const nonce = await salt.getAccountNonce('account-id', 1);
      console.log(`Next nonce: ${nonce}`);
    • Gets the current gas price for a specific chain.

      Parameters

      • chainId: number

        The ID of the chain

      Returns Promise<GasPrice>

      The current gas price

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      InvalidChain if the specified chain is unknown or unsupported

      ApiError if the API request fails

      SyntaxError if the API response contains a malformed numeric value

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const fee = await salt.getGasPrice(1);
      console.log(`Gas for Ethereum Mainnet is between ${fee.lastBaseFeePerGas} and ${fee.maxFeePerGas}`);
    • Gets a single transaction by its ID, including its policy check and — once broadcast — its receipt. The user must have adequate permissions to view the transaction's account.

      Useful for re-reading a transaction after the fact: a ceremony's wait() already returns the final record, so reach for this when you only kept the ID — for example to inspect a transaction proposed in an earlier session or by another signer.

      Parameters

      • transactionId: string

        The ID of the transaction

      Returns Promise<AccountTransaction>

      The transaction record

      InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.

      InsufficientPermissions if you may not read the account the transaction belongs to

      ApiError if the transaction does not exist (404), the ID is malformed (400), or the API returns an error

      import { Salt } from 'salt-sdk';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });

      const transaction = await salt.getTransaction('transaction-id');
      console.log(transaction.state, transaction.broadcastReceipt?.transactionHash);
    • Initiate, sign and broadcast a generic transaction from a given Salt account. This method provides orchestrated transaction handling with automatic account detail fetching, multi-party MPC signing coordination, and policy enforcement by the account's robo signers.

      The Salt account (the destination-chain address derived from its public key) must hold funds to pay gas on the destination chain. The caller's walletClient pays orchestration-chain gas only.

      For sends, if gas, maxFeePerGas, and maxPriorityFeePerGas are not provided, they are populated automatically. For contract deployment, gas is required (the Salt API does not yet estimate gas for deploys).

      A viem publicClient for the destination chain is required: the SDK uses it to broadcast the signed EIP-1559 transaction.

      Important: value is denominated in wei (bigint). Use viem's parseEther('0.5') or parseUnits(amount, decimals) for conversion at the call site.

      End-to-end flow:

      • Build the unsigned EIP-1559 transaction
      • Host an MPC signing session and nudge co-signers + robos (robos refuse on policy breach)
      • Run the DKLS signing ceremony
      • Broadcast via publicClient

      Parameters

      Returns Promise<TransactionHostCeremony>

      A TransactionHostCeremony for orchestrated execution — await its wait() for the final result

      InvalidAuthToken if the auth token is missing or invalid

      InvalidSigner if walletClient is missing or has no attached account

      WrongChain if walletClient is on a chain other than the orchestration chain

      ApiError if the API returns an error (i.e. account does not exist, insufficient permissions to view account)

      SyntaxError if the API response is not valid JSON

      ValidationError if the transaction parameters are invalid

      SaltCeremonyError if the underlying SDK returns an unexpected result (e.g. a contract deployment that fails to produce a valid transaction response)

      InsufficientFunds if the account cannot cover the transaction's total cost (gas + value) on the destination chain — detected either during gas estimation or at broadcast. Paid by the account itself (its SaltAccount.publicKey), not the signer. Subclass of SaltCeremonyError.

      SocketError if the signer fails to connect to websockets, which are required for signing orchestration

      import { Salt } from 'salt-sdk';
      import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia, mainnet } from 'viem/chains';

      const salt = new Salt({
      environment: 'TESTNET',
      authToken: 'your-auth-token'
      });
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
      const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });

      // Encode the approve function call
      const data = encodeFunctionData({
      abi: parseAbi(['function approve(address spender, uint256 amount)']),
      functionName: 'approve',
      args: [
      '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Uniswap V3 SwapRouter
      1000000000n, // 1000 USDC (6 decimals)
      ],
      });

      // Gas and fee data are populated automatically when not provided
      await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC contract
      value: 0n,
      chainId: 1,
      data,
      userAddress: account.address,
      walletClient,
      publicClient,
      });
      import { Salt } from 'salt-sdk';
      import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia, mainnet } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
      const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });

      const data = encodeFunctionData({
      abi: parseAbi(['function approve(address spender, uint256 amount)']),
      functionName: 'approve',
      args: ['0xE592427A0AEce92De3Edee1F18E0157C05861564', 1000000000n],
      });

      await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      value: 0n,
      chainId: 1,
      data,
      userAddress: account.address,
      walletClient,
      publicClient,
      gas: '100000',
      maxFeePerGas: '30000000000', // 30 gwei
      maxPriorityFeePerGas: '1500000000', // 1.5 gwei
      });
      import { Salt } from 'salt-sdk';
      import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi, parseEther } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia, mainnet } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
      const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });

      // Encode the submit function call for Lido staking
      const data = encodeFunctionData({
      abi: parseAbi(['function submit(address _referral) payable returns (uint256)']),
      functionName: 'submit',
      args: ['0x0000000000000000000000000000000000000000'], // No referral
      });

      const tx = await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      to: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', // Lido stETH contract
      value: parseEther('1'), // 1 ETH
      chainId: 1,
      data,
      userAddress: account.address,
      walletClient,
      publicClient,
      });

      const result = await tx.wait();
      import { Salt } from 'salt-sdk';
      import { createPublicClient, createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia, sepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
      const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL) });

      // Omit `to` to submit a contract creation transaction.
      // `data` should contain the compiled contract bytecode; `gas` is required (API does not yet estimate for deploys).
      const tx = await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      value: 0n,
      chainId: 11155111,
      gas: '300000',
      data: '0x608060...', // compiled bytecode
      userAddress: account.address,
      walletClient,
      publicClient,
      });

      const result = await tx.wait();
      const ceremony = await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      to: '0xRecipient',
      value: parseEther('0.1'),
      chainId: 11155111,
      userAddress: account.address,
      walletClient,
      publicClient,
      });

      // Subscribe before awaiting to observe every stage:
      // proposing → signing → broadcasting → confirming → success | failure
      ceremony.on('stateChanged', ({ stage }) => {
      console.log('transaction is now', stage);
      });

      const { transaction } = await ceremony.wait();
      import { Salt, SaltCeremonyError, InsufficientFunds } from 'salt-sdk';
      import { createPublicClient, createWalletClient, http } from 'viem';
      import { privateKeyToAccount } from 'viem/accounts';
      import { arbitrumSepolia, sepolia } from 'viem/chains';

      const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
      const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
      const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
      const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL) });

      try {
      const tx = await salt.submitTx({
      accountId: '0a1b2c3d4e5f',
      value: 0n,
      chainId: 11155111,
      gas: '300000',
      data: '0xDEAD', // not valid contract bytecode
      userAddress: account.address,
      walletClient,
      publicClient,
      });
      await tx.wait();
      } catch (error) {
      if (error instanceof SaltCeremonyError) {
      console.error('Transaction failed:', error.message);
      console.error('Raw result:', error.data);
      } else if (error instanceof InsufficientFunds) {
      console.error(
      `Account needs funding on chain ${error.details.chainId}:`,
      error.details.accountAddress ?? error.details.accountId
      );
      } else {
      throw error;
      }
      }