This guide shows how to create Swig wallets programmatically using the Developer Portal API. This approach is ideal for applications that need to create wallets on behalf of users with predefined policies.
When to Use the API
Use the Portal API when:
- Creating wallets from a backend service
- Using predefined policies from the Developer Portal
- Leveraging the paymaster for sponsored wallet creation (optional)
- You want to build the transaction server-side but sign and send it client-side
- Managing wallets at scale
Use the SDK directly when:
- Building client-side applications
- Users control their own keypairs
- You need full control over wallet configuration
- Not using the Developer Portal policies
Prerequisites
Before creating wallets via the API, ensure you have:
A paymaster is optional. When you omit paymasterPubkey, the API returns an unsigned transaction instead of submitting it on-chain. You can then sign and send the transaction yourself. See Creating Without a Paymaster below.
Installing the SDK
npm install @swig-wallet/api
# or
bun add @swig-wallet/api
npm install @swig-wallet/developer
# or
bun add @swig-wallet/developer
Specifying the Wallet Authority
When creating a Swig wallet, you need to define the authority (signer) that will control it. There are two ways to do this:
Option 1: Using a Signer ID from the Dashboard
If you’ve already created a signer in the Developer Portal dashboard (or your policy has one attached), you can reference it by its signerId. This is the original approach and is useful when you manage signers centrally through the portal.
Option 2: Providing a Wallet Address and Type Directly
You can supply a walletAddress (the public key) and walletType (the authority type) at creation time instead of using a signer ID. This lets you skip creating a signer in the dashboard entirely — useful when your application already knows the user’s public key and authority type.
You must use one approach or the other — provide either signerId or the walletAddress + walletType pair. If your policy already has a signer attached and you don’t pass either, the policy’s signer is used by default.
Supported Wallet Types
When using the walletAddress + walletType approach, the following types are supported:
| Type | Description |
|---|
ED25519 | Standard Solana keypair |
ED25519_SESSION | Time-limited Ed25519 session |
SECP256K1 | Ethereum-compatible keys |
SECP256K1_SESSION | Time-limited Secp256k1 session |
SECP256R1 | WebAuthn/Passkey compatible |
SECP256R1_SESSION | Time-limited Secp256r1 session |
For session types (ED25519_SESSION, SECP256K1_SESSION, SECP256R1_SESSION), you must also provide the maxDurationSlots parameter to specify how long the session authority remains valid.
Creating a Swig Wallet
Using the REST API
Make a POST request to the wallet creation endpoint:
curl -X POST "https://dashboard.onswig.com/api/v1/wallet/create" \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"policyId": "clx1234567890abcdef",
"network": "devnet",
"paymasterPubkey": "PaymasterPublicKeyHere..."
}'
You can also supply the wallet authority directly instead of relying on a signer from the dashboard:
curl -X POST "https://dashboard.onswig.com/api/v1/wallet/create" \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"policyId": "clx1234567890abcdef",
"network": "devnet",
"paymasterPubkey": "PaymasterPublicKeyHere...",
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"walletType": "ED25519"
}'
Example Response:
{
"swigId": "a1b2c3d4e5f67890",
"swigAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"signature": "5wHu1qwD7g4p3zWxF..."
}
Using the SDK
import { SwigApiClient } from '@swig-wallet/api';
// Initialize the client
const client = new SwigApiClient({
apiKey: 'sk_your_api_key',
portalUrl: 'https://dashboard.onswig.com',
});
// Create a wallet
async function createWallet() {
const { data: wallet, error } = await client.wallet.create({
policyId: 'clx1234567890abcdef',
network: 'devnet',
paymasterPubkey: 'PaymasterPublicKeyHere...',
});
if (error) {
console.error(`Error creating wallet: ${error.message}`);
return null;
}
console.log('Swig ID:', wallet.swigId);
console.log('Wallet Address:', wallet.swigAddress);
console.log('Transaction Signature:', wallet.signature);
return wallet;
}
const wallet = await createWallet();
import { SwigClient, SwigError } from '@swig-wallet/developer';
// Initialize the client
const client = new SwigClient({
apiKey: 'sk_your_api_key',
baseUrl: 'https://dashboard.onswig.com',
});
// Create a wallet
async function createWallet() {
try {
const wallet = await client.createWallet({
policyId: 'clx1234567890abcdef',
network: 'devnet',
paymasterPubkey: 'PaymasterPublicKeyHere...',
});
console.log('Swig ID:', wallet.swigId);
console.log('Wallet Address:', wallet.swigAddress);
console.log('Transaction Signature:', wallet.signature);
return wallet;
} catch (error) {
if (error instanceof SwigError) {
console.error(`Error: ${error.code} - ${error.message}`);
}
return null;
}
}
const wallet = await createWallet();
With Wallet Address and Type
You can also provide a walletAddress and walletType directly instead of relying on a signer configured in the dashboard:
import { SwigApiClient } from '@swig-wallet/api';
const client = new SwigApiClient({
apiKey: 'sk_your_api_key',
portalUrl: 'https://dashboard.onswig.com',
});
async function createWallet() {
const { data: wallet, error } = await client.wallet.create({
policyId: 'clx1234567890abcdef',
network: 'devnet',
paymasterPubkey: 'PaymasterPublicKeyHere...',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519',
});
if (error) {
console.error(`Error creating wallet: ${error.message}`);
return null;
}
console.log('Swig ID:', wallet.swigId);
console.log('Wallet Address:', wallet.swigAddress);
console.log('Transaction Signature:', wallet.signature);
return wallet;
}
const wallet = await createWallet();
import { SwigClient, SwigError } from '@swig-wallet/developer';
const client = new SwigClient({
apiKey: 'sk_your_api_key',
baseUrl: 'https://dashboard.onswig.com',
});
async function createWallet() {
try {
const wallet = await client.createWallet({
policyId: 'clx1234567890abcdef',
network: 'devnet',
paymasterPubkey: 'PaymasterPublicKeyHere...',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519',
});
console.log('Swig ID:', wallet.swigId);
console.log('Wallet Address:', wallet.swigAddress);
console.log('Transaction Signature:', wallet.signature);
return wallet;
} catch (error) {
if (error instanceof SwigError) {
console.error(`Error: ${error.code} - ${error.message}`);
}
return null;
}
}
const wallet = await createWallet();
SDK Comparison
| Feature | @swig-wallet/api | @swig-wallet/developer |
|---|
| Error handling | Returns { data, error } | Throws SwigError |
| Method name | client.wallet.create() | client.createWallet() |
| No-paymaster response | Raw base58 string in transaction | Reconstructed VersionedTransaction object |
| Best for | Low-level control | Developer convenience |
Request Parameters
Required Parameters
| Parameter | Type | Description |
|---|
policyId | string | The policy ID from the Developer Portal |
network | 'mainnet' | 'devnet' | Target Solana network |
Optional Parameters
| Parameter | Type | Description |
|---|
paymasterPubkey | string | Public key of the paymaster covering fees. When provided, the paymaster signs and sends the transaction. When omitted, the API returns an unsigned transaction for you to sign and send. |
swigId | string | Custom Swig ID (auto-generated if not provided) |
signerId | string | Signer ID from the dashboard (alternative to walletAddress + walletType) |
walletAddress | string | Wallet public key to use as the authority (alternative to signerId). Must be provided together with walletType. |
walletType | WalletType | Authority type for the wallet (alternative to signerId). Must be provided together with walletAddress. One of: ED25519, ED25519_SESSION, SECP256K1, SECP256K1_SESSION, SECP256R1, SECP256R1_SESSION. |
maxDurationSlots | string | Maximum session duration in slots. Only valid for session authority types (ED25519_SESSION, SECP256K1_SESSION, SECP256R1_SESSION). |
Response Fields
The response shape depends on whether a paymaster was provided.
With Paymaster
| Field | Type | Description |
|---|
swigId | string | The unique identifier for this Swig wallet |
swigAddress | string | The on-chain PDA address of the wallet |
signature | string | The transaction signature (viewable on explorer) |
Without Paymaster
| Field | Type | Description |
|---|
swigId | string | The unique identifier for this Swig wallet |
swigAddress | string | The on-chain PDA address of the wallet |
transaction | string | Base58-encoded serialized VersionedTransaction (unsigned) |
When no paymaster is used, the expected swig address is still recorded in the Developer Portal database so you can track and manage it through the dashboard.
Creating Without a Paymaster
When you omit paymasterPubkey, the API builds the create-swig transaction but does not send it. Instead, it returns a base58-encoded serialized VersionedTransaction that you can deserialize, sign, and send yourself.
This is useful when:
- Your users pay their own transaction fees
- You want to collect multiple signatures before sending
- You don’t want to set up or fund a paymaster
Using the REST API (No Paymaster)
curl -X POST "https://dashboard.onswig.com/api/v1/wallet/create" \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"policyId": "clx1234567890abcdef",
"network": "devnet"
}'
Example Response:
{
"data": {
"swigId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"swigAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"transaction": "BASE58_ENCODED_VERSIONED_TRANSACTION..."
},
"error": null
}
Using the SDK (No Paymaster)
import { SwigApiClient } from '@swig-wallet/api';
const client = new SwigApiClient({
apiKey: 'sk_your_api_key',
portalUrl: 'https://dashboard.onswig.com',
});
// Create without paymaster — returns the raw transaction data
const { data, error } = await client.wallet.create({
policyId: 'clx1234567890abcdef',
network: 'devnet',
// No paymasterPubkey
});
if (error) {
console.error(`Error: ${error.message}`);
} else if (data && 'transaction' in data) {
console.log('Swig ID:', data.swigId);
console.log('Swig Address:', data.swigAddress);
console.log('Transaction (base58):', data.transaction);
// Reconstruct, sign, and send the transaction yourself:
// const txBytes = bs58.decode(data.transaction);
// const tx = VersionedTransaction.deserialize(txBytes);
// tx.sign([yourKeypair]);
// const sig = await connection.sendTransaction(tx);
}
import { Connection } from '@solana/web3.js';
import { SwigClient, SwigError } from '@swig-wallet/developer';
const client = new SwigClient({
apiKey: 'sk_your_api_key',
baseUrl: 'https://dashboard.onswig.com',
});
try {
// Create without paymaster — returns a VersionedTransaction object
const result = await client.createWallet({
policyId: 'clx1234567890abcdef',
network: 'devnet',
// No paymasterPubkey
});
if ('transaction' in result) {
console.log('Swig ID:', result.swigId);
console.log('Swig Address:', result.swigAddress);
// The SDK automatically reconstructs the VersionedTransaction
const tx = result.transaction; // VersionedTransaction object
// Sign with your keypair and send
tx.sign([yourKeypair]);
const connection = new Connection('https://api.devnet.solana.com');
const signature = await connection.sendTransaction(tx);
console.log('Transaction sent:', signature);
}
} catch (error) {
if (error instanceof SwigError) {
console.error(`Error: ${error.code} - ${error.message}`);
}
}
When using the no-paymaster flow, the returned transaction includes a recent blockhash. Blockhashes expire after approximately 60-90 seconds. You must sign and send the transaction promptly.
Examples
Basic Wallet Creation
import { SwigApiClient } from '@swig-wallet/api';
const client = new SwigApiClient({
apiKey: process.env.SWIG_API_KEY!,
portalUrl: 'https://dashboard.onswig.com',
});
const { data, error } = await client.wallet.create({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
});
if (data) {
console.log(`Wallet created: ${data.swigAddress}`);
console.log(`View on explorer: https://explorer.solana.com/address/${data.swigAddress}?cluster=devnet`);
}
import { SwigClient } from '@swig-wallet/developer';
const client = new SwigClient({
apiKey: process.env.SWIG_API_KEY!,
baseUrl: 'https://dashboard.onswig.com',
});
const wallet = await client.createWallet({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
});
console.log(`Wallet created: ${wallet.swigAddress}`);
console.log(`View on explorer: https://explorer.solana.com/address/${wallet.swigAddress}?cluster=devnet`);
With Custom Swig ID
// Generate a custom ID (must be unique)
const customId = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
const { data, error } = await client.wallet.create({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
swigId: customId,
});
// Generate a custom ID (must be unique)
const customId = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
const wallet = await client.createWallet({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
swigId: customId,
});
With Signer Override
If your policy doesn’t have a signer attached, or you want to use a different signer from the dashboard:
const { data, error } = await client.wallet.create({
policyId: 'policy-without-signer',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
signerId: 'specific-signer-id', // Signer ID from the portal
});
const wallet = await client.createWallet({
policyId: 'policy-without-signer',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
signerId: 'specific-signer-id', // Signer ID from the portal
});
With Wallet Address and Type
Instead of creating a signer in the dashboard and referencing it by ID, you can supply the wallet’s public key and authority type directly at creation time:
const { data, error } = await client.wallet.create({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519',
});
const wallet = await client.createWallet({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519',
});
With Session Authority
For session-based authority types, include maxDurationSlots to set the session lifetime:
const { data, error } = await client.wallet.create({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519_SESSION',
maxDurationSlots: '1000000', // session duration in slots
});
const wallet = await client.createWallet({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
walletAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU',
walletType: 'ED25519_SESSION',
maxDurationSlots: '1000000', // session duration in slots
});
Production Wallet
const { data, error } = await client.wallet.create({
policyId: 'production-policy-id',
network: 'mainnet', // Use mainnet for production
paymasterPubkey: 'ProductionPaymasterKey',
});
if (data) {
// Store the wallet info in your database
await db.wallets.create({
swigId: data.swigId,
address: data.swigAddress,
createdAt: new Date(),
});
}
const wallet = await client.createWallet({
policyId: 'production-policy-id',
network: 'mainnet', // Use mainnet for production
paymasterPubkey: 'ProductionPaymasterKey',
});
// Store the wallet info in your database
await db.wallets.create({
swigId: wallet.swigId,
address: wallet.swigAddress,
createdAt: new Date(),
});
Error Handling
const { data, error } = await client.wallet.create({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
});
if (error) {
switch (error.code) {
case 'NOT_FOUND':
console.error('Policy not found. Check the policy ID.');
break;
case 'UNAUTHORIZED':
console.error('Invalid API key.');
break;
case 'BAD_REQUEST':
console.error('Invalid request:', error.details);
break;
case 'CONFLICT':
console.error('Swig ID already exists. Try a different ID.');
break;
default:
console.error(`Unexpected error: ${error.message}`);
}
}
try {
const wallet = await client.createWallet({
policyId: 'your-policy-id',
network: 'devnet',
paymasterPubkey: 'YourPaymasterPublicKey',
});
} catch (error) {
if (error instanceof SwigError) {
switch (error.code) {
case 'POLICY_NOT_FOUND':
console.error('Policy not found. Check the policy ID.');
break;
case 'UNAUTHORIZED':
console.error('Invalid API key.');
break;
case 'BAD_REQUEST':
console.error('Invalid request:', error.response);
break;
case 'SWIG_ID_EXISTS':
console.error('Swig ID already exists. Try a different ID.');
break;
default:
console.error(`Unexpected error: ${error.message}`);
}
}
}
Common Errors
| Status | Code | Meaning |
|---|
| 400 | BAD_REQUEST | Invalid request parameters |
| 401 | UNAUTHORIZED | Invalid or missing API key |
| 404 | NOT_FOUND / POLICY_NOT_FOUND | Policy or signer not found |
| 409 | CONFLICT / SWIG_ID_EXISTS | Swig ID already exists |
| 500 | INTERNAL_ERROR | Server error, try again |
Complete Example
import { SwigApiClient } from '@swig-wallet/api';
async function main() {
const client = new SwigApiClient({
apiKey: process.env.SWIG_API_KEY!,
portalUrl: 'https://dashboard.onswig.com',
retry: { maxRetries: 3, retryDelay: 1000 },
});
const config = {
policyId: process.env.SWIG_POLICY_ID!,
network: (process.env.SWIG_NETWORK || 'devnet') as 'mainnet' | 'devnet',
paymasterPubkey: process.env.SWIG_PAYMASTER_PUBKEY!,
};
console.log('Creating Swig wallet...');
console.log(`Network: ${config.network}`);
console.log(`Policy: ${config.policyId}`);
const { data: wallet, error } = await client.wallet.create(config);
if (error) {
console.error('Failed to create wallet:', error.message);
console.error('Error code:', error.code);
process.exit(1);
}
console.log('\n=== Wallet Created ===');
console.log(`Swig ID: ${wallet.swigId}`);
console.log(`Address: ${wallet.swigAddress}`);
console.log(`Transaction: ${wallet.signature}`);
const cluster = config.network === 'mainnet' ? '' : '?cluster=devnet';
console.log(`\nExplorer: https://explorer.solana.com/address/${wallet.swigAddress}${cluster}`);
}
main();
import { SwigClient, SwigError } from '@swig-wallet/developer';
async function main() {
const client = new SwigClient({
apiKey: process.env.SWIG_API_KEY!,
baseUrl: 'https://dashboard.onswig.com',
retryOptions: { maxRetries: 3, retryDelay: 1000 },
});
const config = {
policyId: process.env.SWIG_POLICY_ID!,
network: (process.env.SWIG_NETWORK || 'devnet') as 'mainnet' | 'devnet',
paymasterPubkey: process.env.SWIG_PAYMASTER_PUBKEY!,
};
console.log('Creating Swig wallet...');
console.log(`Network: ${config.network}`);
console.log(`Policy: ${config.policyId}`);
try {
const wallet = await client.createWallet(config);
console.log('\n=== Wallet Created ===');
console.log(`Swig ID: ${wallet.swigId}`);
console.log(`Address: ${wallet.swigAddress}`);
console.log(`Transaction: ${wallet.signature}`);
const cluster = config.network === 'mainnet' ? '' : '?cluster=devnet';
console.log(`\nExplorer: https://explorer.solana.com/address/${wallet.swigAddress}${cluster}`);
} catch (error) {
if (error instanceof SwigError) {
console.error('Failed to create wallet:', error.message);
console.error('Error code:', error.code);
process.exit(1);
}
throw error;
}
}
main();
TypeScript Types
type WalletType =
| 'ED25519'
| 'ED25519_SESSION'
| 'SECP256K1'
| 'SECP256K1_SESSION'
| 'SECP256R1'
| 'SECP256R1_SESSION';
interface CreateWalletRequest {
/** Policy ID to use for wallet creation */
policyId: string;
/** Optional signer ID to override or provide (alternative to walletAddress + walletType) */
signerId?: string;
/**
* Wallet public key to use as the authority (alternative to signerId).
* Must be provided together with walletType.
*/
walletAddress?: string;
/**
* Authority type for the wallet (alternative to signerId).
* Must be provided together with walletAddress.
*/
walletType?: WalletType;
/**
* Maximum session duration in slots (only valid for session authority types).
* String to safely handle bigint values.
*/
maxDurationSlots?: string;
/** Optional swig ID (generated if not provided) */
swigId?: string;
/** Network to use */
network: 'mainnet' | 'devnet';
/** Paymaster public key (optional — omit for self-send flow) */
paymasterPubkey?: string;
}
/** Response when a paymaster is used */
interface CreateWalletPaymasterResponse {
swigId: string;
swigAddress: string;
signature: string;
}
/** Response when no paymaster is provided */
interface CreateWalletTransactionResponse {
swigId: string;
swigAddress: string;
/** Base58-encoded serialized VersionedTransaction (unsigned) */
transaction: string;
}
type CreateWalletResponse =
| CreateWalletPaymasterResponse
| CreateWalletTransactionResponse;
Developer SDK Types
The @swig-wallet/developer SDK provides higher-level types that automatically reconstruct the transaction:
import type { VersionedTransaction } from '@solana/web3.js';
/** Result when a paymaster was used */
interface WalletCreatePaymasterResult {
swigId: string;
swigAddress: string;
signature: string;
}
/** Result when no paymaster was provided */
interface WalletCreateTransactionResult {
swigId: string;
swigAddress: string;
/** Reconstructed VersionedTransaction ready for signing */
transaction: VersionedTransaction;
}
type WalletCreateResult =
| WalletCreatePaymasterResult
| WalletCreateTransactionResult;
Comparison: Portal API vs Direct SDK
| Feature | Portal API | Direct SDK |
|---|
| Uses predefined policies | Yes | No |
| Requires API key | Yes | No |
| Paymaster sponsored | Optional | Optional |
| Self-send (no paymaster) | Yes — returns unsigned tx | Yes — you build the tx |
| Full configuration control | Limited to policy | Full |
| Best for | Backend services | Client apps |
| Keypair management | Server-side | Client-side |
Next Steps
After creating a wallet, you can: