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 userIn 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 signatureUse 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.
| Status | Meaning |
|---|---|
pending_step_up | Needs a passkey approval first. |
queued → processing | Accepted, then being signed and sent. |
completed | result holds the signature or transaction hash. |
failed | Rejected 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.