docs
Get startedTroubleshootingPaymentsWebhooksAgent paymentsSwop MCPOviIdentity (swop.id)SwivyRPC (preview)API reference

Wallets & signing

Create EVM and Solana wallets, sign messages and EIP-712 typed data, and send transactions as intents.

Preview: Swivy is not generally available

Swivy runs in staging only and the SDKs aren't on npm yet. Method names below come from the @swivy/web, @swivy/react and @swivy/node source. Request early access.

In plain words: a wallet is an address plus a private key. With Swivy the key stays in the signer (or, in split custody, partly in the user's browser). Your app asks for a signature by submitting an intent, the user approves it with a passkey when policy says so, and you get back the result.

Create or load wallets

Wallets are usually created at login, as your policy allows. To make sure one exists:

const evm = await swivy.ensureEmbeddedWallet('evm', { label: 'Primary wallet' });
const sol = await swivy.ensureEmbeddedWallet('solana', { label: 'Primary wallet' });
const wallets = await swivy.listEmbeddedWallets(); // every managed account for this user

In React, useEmbeddedWallets('evm') gives you wallets and ensureManagedAccount('evm').

Approve first, then sign

If policy requires step-up, get a challengeId from a passkey approval and attach it to the intent:

const approval = await swivy.approveWithPasskey(passkeyAuthMethodId, 'intent_approval');
const challengeId = approval.challenge.challengeId;

Sign a message

const intent = await swivy.signMessageAndWait({
  accountId: evm.account.id,
  chain: 'evm',
  challengeId,
  payload: { message: 'Sign in to Acme' },
});
console.log(intent.result); // includes the signature

Use messageHex or messageBase64 in payload for binary messages. On Solana pass chain: 'solana'.

Sign EIP-712 typed data (EVM)

const intent = await swivy.signTypedDataAndWait({
  accountId: evm.account.id,
  challengeId,
  typedData: {
    domain: { name: 'Acme', version: '1', chainId: 8453 },
    types: { Order: [{ name: 'amount', type: 'uint256' }] },
    primaryType: 'Order',
    message: { amount: '1000000' },
  },
});

Sign a transaction without sending it

const intent = await swivy.signTransactionAndWait({
  accountId: evm.account.id,
  chain: 'evm',
  challengeId,
  payload: { transaction: { to: '0x1111111111111111111111111111111111111111', value: '0x0', data: '0x' } },
});

For Solana, pass serializedTransactionBase64 instead of transaction.

Send a transaction (execute)

Swivy signs, optionally sponsors gas, broadcasts through your configured RPC and reports back:

const intent = await swivy.createTransactionIntentAndWait({
  accountId: evm.account.id,
  chain: 'evm',
  challengeId,
  payload: {
    network: 'base-mainnet',
    transaction: { to: '0x1111111111111111111111111111111111111111', value: '0x0', data: '0x' },
  },
});
console.log(intent.status, intent.result);

payload.network must be a network your environment has an RPC for: base-mainnet, ethereum-mainnet, arbitrum-mainnet, optimism-mainnet, polygon-mainnet, solana-mainnet, base-sepolia or solana-devnet.

Intent lifecycle

Every call above creates an intent. The ...AndWait helpers poll until it finishes; you can also create one with createSigningIntent or createTransactionIntent, then call getIntent(id) or waitForIntent(id), or listen for the webhook.

StatusMeaning
pending_step_upNeeds a passkey approval first.
queued → processingAccepted, then being signed and sent.
completedresult holds the signature or transaction hash.
failedRejected by policy or failed on chain.

Split-custody wallets: sign in the browser

For split_custody wallets Swivy can't sign alone. Sign locally after a passkey unlock:

const signed = await swivy.signMessageLocallyWithPasskey(
  { accountId: wallet.id, chain: 'evm', payload: { message: 'approve local signing' } },
  passkeyAuthMethodId,
);
console.log(signed.result.signature);

signTypedDataLocallyWithPasskey and signTransactionLocallyWithPasskey work the same way. See the React quickstart for the full self-custody flow.

Export and raw signing

exportManagedAccount and rawSign are escape hatches for hybrid setups. Both are off unless your policy enables them and need a step-up challenge (account_export for export). Prefer intents.

On this page