skills/swig-smart-wallet/SKILL.md
Create and manage Solana smart wallets using the Swig TypeScript SDK. This skill enables AI agents to generate Swig wallets, manage authorities (add/remove/update with granular permissions), execute transactions through the wallet, and handle gas sponsorship via the Swig Paymaster API, a custom gas server, or self-funded SOL. Works with both @swig-wallet/classic (web3.js 1.x) and @swig-wallet/kit (@solana/kit 2.x).
npx skillsauth add anagrambuild/swig-ts swig-smart-walletInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an AI agent that can create and manage Solana smart wallets using the Swig protocol. Swig wallets are on-chain programmable wallets with granular authority and permission management. You write TypeScript scripts that use the Swig SDK to perform wallet operations.
Before doing anything, you MUST gather the following from the user:
Ask the user: "What Solana RPC endpoint should I use for transactions?"
Acceptable answers:
https://api.mainnet-beta.solana.com, https://api.devnet.solana.com, or a custom RPC like Helius, Triton, etc.)https://api.devnet.solana.comhttps://api.mainnet-beta.solana.comhttp://localhost:8899If the user does not know, suggest they use devnet for testing: https://api.devnet.solana.com
Store the RPC URL in an environment variable SOLANA_RPC_URL or in a .env file.
Ask the user: "How would you like transaction fees to be handled?"
Present these three options:
Option A — Swig Paymaster (recommended for production) The Swig Paymaster API covers transaction fees. Ask the user to provide:
dashboard.onswig.comdashboard.onswig.commainnet or devnetStore these as environment variables:
SWIG_PAYMASTER_API_KEY=<their-api-key>
SWIG_PAYMASTER_PUBKEY=<their-paymaster-pubkey>
SWIG_PAYMASTER_NETWORK=devnet
Option B — Custom Gas Sponsorship Server The user runs their own server that signs/sponsors transactions. Ask for:
Store as:
GAS_SPONSOR_URL=<their-server-url>
Option C — Self-funded (agent pays its own fees) The agent generates its own Solana keypair and pays fees from its own balance. The user must send a small amount of SOL (0.01-0.1 SOL) to the agent's address.
When using this option:
agent-keypair.json)<agent-public-key> and let me know when done."The Swig SDK comes in two flavors. Choose based on the project:
| Package | Solana SDK | When to Use |
| ---------------------- | ---------------------- | ------------------------------------------------------------------- |
| @swig-wallet/classic | @solana/web3.js v1.x | Existing projects using web3.js v1, broader ecosystem compatibility |
| @swig-wallet/kit | @solana/kit v2.x | New projects, modern Solana development |
If the user has no preference, default to @swig-wallet/classic for broader compatibility.
# For classic (web3.js 1.x)
npm install @swig-wallet/classic @solana/web3.js
# For kit (@solana/kit 2.x)
npm install @swig-wallet/kit @solana/kit
# For paymaster support (add one based on SDK choice)
npm install @swig-wallet/paymaster-classic
# or
npm install @swig-wallet/paymaster-kit
A Swig wallet is created on-chain with:
Actions.set().all().get() for full control)import {
Connection,
Keypair,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import {
Actions,
createEd25519AuthorityInfo,
fetchSwig,
findSwigPda,
getCreateSwigInstruction,
getSwigWalletAddress,
} from '@swig-wallet/classic';
const connection = new Connection(process.env.SOLANA_RPC_URL!, 'confirmed');
const payer = Keypair.fromSecretKey(/* loaded from file or env */);
// 1. Generate random 32-byte Swig ID
const id = new Uint8Array(32);
crypto.getRandomValues(id);
// 2. Derive the Swig account PDA
const swigAccountAddress = findSwigPda(id);
// 3. Create root authority info from the payer's pubkey
const rootAuthorityInfo = createEd25519AuthorityInfo(payer.publicKey);
// 4. Set root permissions (full control)
const rootActions = Actions.set().all().get();
// 5. Build the create instruction
const createSwigIx = await getCreateSwigInstruction({
payer: payer.publicKey,
id,
actions: rootActions,
authorityInfo: rootAuthorityInfo,
});
// 6. Send transaction
const tx = new Transaction().add(createSwigIx);
const signature = await sendAndConfirmTransaction(connection, tx, [payer]);
// 7. Fetch and verify
const swig = await fetchSwig(connection, swigAccountAddress);
const walletAddress = await getSwigWalletAddress(swig);
console.log('Swig account:', swigAccountAddress.toBase58());
console.log('Swig wallet address:', walletAddress.toBase58());
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
generateKeyPairSigner,
sendAndConfirmTransactionFactory,
signTransaction,
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
compileTransaction,
getSignatureFromTransaction,
} from '@solana/kit';
import {
Actions,
createEd25519AuthorityInfo,
fetchSwig,
findSwigPda,
getCreateSwigInstruction,
getSwigWalletAddress,
} from '@swig-wallet/kit';
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!);
const rpcSubscriptions = createSolanaRpcSubscriptions(
process.env.SOLANA_RPC_URL!.replace('https', 'wss'),
);
const payer = await generateKeyPairSigner();
const id = new Uint8Array(32);
crypto.getRandomValues(id);
const swigAccountAddress = await findSwigPda(id);
const rootAuthorityInfo = createEd25519AuthorityInfo(payer.address);
const rootActions = Actions.set().all().get();
const createSwigIx = await getCreateSwigInstruction({
payer: payer.address,
id,
actions: rootActions,
authorityInfo: rootAuthorityInfo,
});
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const txMessage = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(payer.address, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) => appendTransactionMessageInstructions([createSwigIx], m),
);
const compiledTx = compileTransaction(txMessage);
const signedTx = await signTransaction([payer.keyPair], compiledTx);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTx, {
commitment: 'confirmed',
});
const swig = await fetchSwig(rpc, swigAccountAddress);
const walletAddress = await getSwigWalletAddress(swig);
Authorities are additional keys that can interact with the Swig wallet, each with specific permissions.
import {
fetchSwig,
getAddAuthorityInstructions,
createEd25519AuthorityInfo,
Actions,
} from '@swig-wallet/classic';
const swig = await fetchSwig(connection, swigAccountAddress);
const rootRole = swig.findRolesByEd25519SignerPk(rootKeypair.publicKey)[0];
// Create a new authority with specific permissions
const newAuthorityInfo = createEd25519AuthorityInfo(newKeypair.publicKey);
// Example permissions: 0.5 SOL spend limit
const actions = Actions.set()
.solLimit({ amount: 500_000_000n }) // 0.5 SOL in lamports
.get();
const ixs = await getAddAuthorityInstructions(
swig,
rootRole.id,
newAuthorityInfo,
actions,
);
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [rootKeypair]);
import {
getRemoveAuthorityInstructions,
fetchSwig,
} from '@swig-wallet/classic';
const swig = await fetchSwig(connection, swigAccountAddress);
const rootRole = swig.findRolesByEd25519SignerPk(rootKeypair.publicKey)[0];
// Find the role ID of the authority to remove
const roleToRemove = swig.findRolesByEd25519SignerPk(targetPubkey)[0];
const ixs = await getRemoveAuthorityInstructions(
swig,
rootRole.id,
roleToRemove.id,
);
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [rootKeypair]);
import {
getUpdateAuthorityInstructions,
updateAuthorityReplaceAllActions,
updateAuthorityAddActions,
updateAuthorityRemoveByType,
Actions,
Permission,
fetchSwig,
} from '@swig-wallet/classic';
const swig = await fetchSwig(connection, swigAccountAddress);
const rootRole = swig.findRolesByEd25519SignerPk(rootKeypair.publicKey)[0];
const roleToUpdate = swig.findRolesByEd25519SignerPk(targetPubkey)[0];
// Option 1: Replace all actions
const updateInfo = updateAuthorityReplaceAllActions(
Actions.set().solLimit({ amount: 1_000_000_000n }).programAll().get(),
);
// Option 2: Add new actions
// const updateInfo = updateAuthorityAddActions(
// Actions.set().tokenLimit({ mint: tokenMintPubkey, amount: 1_000_000n }).get()
// );
// Option 3: Remove actions by type
// const updateInfo = updateAuthorityRemoveByType([Permission.SolLimit]);
const ixs = await getUpdateAuthorityInstructions(
swig,
rootRole.id,
roleToUpdate.id,
updateInfo,
);
const tx = new Transaction().add(...ixs);
await sendAndConfirmTransaction(connection, tx, [rootKeypair]);
The Swig wallet acts as the signer for inner instructions. Use getSignInstructions to wrap any instruction so it executes from the Swig wallet.
import { SystemProgram } from '@solana/web3.js';
import {
getSignInstructions,
getSwigWalletAddress,
fetchSwig,
} from '@swig-wallet/classic';
const swig = await fetchSwig(connection, swigAccountAddress);
const walletAddress = await getSwigWalletAddress(swig);
const role = swig.findRolesByEd25519SignerPk(signerKeypair.publicKey)[0];
// Build the inner instruction (SOL transfer from the Swig wallet)
const transferIx = SystemProgram.transfer({
fromPubkey: walletAddress,
toPubkey: recipientPubkey,
lamports: 100_000_000, // 0.1 SOL
});
// Wrap it with Swig signing
const signedIxs = await getSignInstructions(swig, role.id, [transferIx]);
const tx = new Transaction().add(...signedIxs);
await sendAndConfirmTransaction(connection, tx, [signerKeypair]);
import { Keypair } from '@solana/web3.js';
import { createPaymasterClient } from '@swig-wallet/paymaster-classic';
const paymaster = createPaymasterClient({
apiKey: process.env.SWIG_PAYMASTER_API_KEY!,
paymasterPubkey: process.env.SWIG_PAYMASTER_PUBKEY!,
baseUrl: 'https://api.onswig.com',
network: process.env.SWIG_PAYMASTER_NETWORK as 'mainnet' | 'devnet',
});
// Build your instructions normally, then:
const tx = await paymaster.createLegacyTransaction(
[instruction1, instruction2],
[userKeypair], // user signs
);
// Paymaster signs and sends
const signature = await paymaster.signAndSend(tx);
import { partiallySignTransaction } from '@solana/kit';
import { createPaymasterClient } from '@swig-wallet/paymaster-kit';
const paymaster = createPaymasterClient({
apiKey: process.env.SWIG_PAYMASTER_API_KEY!,
paymasterPubkey: process.env.SWIG_PAYMASTER_PUBKEY!,
baseUrl: 'https://api.onswig.com',
network: process.env.SWIG_PAYMASTER_NETWORK as 'mainnet' | 'devnet',
});
const unsignedTx = await paymaster.createTransaction([instruction]);
const partiallySignedTx = await partiallySignTransaction(
[userKeypair.keyPair],
unsignedTx,
);
const fullySignedTx = await paymaster.fullySign(partiallySignedTx);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
fullySignedTx,
{ commitment: 'confirmed' },
);
If the user has their own gas server, send the serialized transaction to it for signing:
async function sponsorTransaction(
serializedTx: Uint8Array,
sponsorUrl: string,
): Promise<Uint8Array> {
const response = await fetch(`${sponsorUrl}/sponsor`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transaction: Buffer.from(serializedTx).toString('base64'),
}),
});
const { signedTransaction } = await response.json();
return Buffer.from(signedTransaction, 'base64');
}
Use the Actions builder to set permissions for authorities:
| Method | Description |
| --------------------------------------------------------------------------------- | ------------------------------------------------ |
| .all() | Full root permissions (everything) |
| .manageAuthority() | Can add/remove/update other authorities |
| .allButManageAuthority() | Everything except managing authorities |
| .closeSwigAuthority() | Can close the Swig account |
| .solLimit({ amount }) | One-time SOL spend limit (in lamports as bigint) |
| .solRecurringLimit({ recurringAmount, window }) | Recurring SOL limit (window in slots) |
| .solDestinationLimit({ amount, destination }) | SOL limit to specific recipient |
| .solRecurringDestinationLimit({ recurringAmount, window, destination }) | Recurring SOL to specific recipient |
| .tokenLimit({ mint, amount }) | One-time token spend limit |
| .tokenRecurringLimit({ mint, recurringAmount, window }) | Recurring token limit |
| .tokenDestinationLimit({ mint, amount, destination }) | Token limit to specific recipient |
| .tokenRecurringDestinationLimit({ mint, recurringAmount, window, destination }) | Recurring token to specific recipient |
| .programLimit({ programId }) | Can interact with a specific program |
| .programAll() | Can interact with any program |
| .programCurated() | Can interact with curated programs |
| .subAccount() | Can create/manage sub-accounts |
| .stakeAll() | Full staking permissions |
| .stakeLimit({ amount }) | Staking with amount limit |
Combine permissions by chaining:
const actions = Actions.set()
.solLimit({ amount: 1_000_000_000n }) // 1 SOL limit
.tokenLimit({ mint: usdcMint, amount: 100_000_000n }) // 100 USDC
.programLimit({ programId: jupiterProgramId }) // Jupiter access
.get();
| Type | Function | Use Case |
| --------------- | ----------------------------------------------------------- | ----------------------- |
| Ed25519 | createEd25519AuthorityInfo(publicKey) | Standard Solana keypair |
| Ed25519 Session | createEd25519SessionAuthorityInfo(publicKey, maxDuration) | Time-limited Ed25519 |
| Secp256k1 | createSecp256k1AuthorityInfo(publicKey) | Ethereum-style keys |
| Secp256r1 | createSecp256r1AuthorityInfo(publicKey) | Passkeys / WebAuthn |
When the agent needs its own keypair:
import { Keypair } from '@solana/web3.js';
import fs from 'fs';
// Generate and save
const agentKeypair = Keypair.generate();
fs.writeFileSync(
'agent-keypair.json',
JSON.stringify(Array.from(agentKeypair.secretKey)),
);
console.log('Agent public key:', agentKeypair.publicKey.toBase58());
// Load later
const loaded = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync('agent-keypair.json', 'utf-8'))),
);
swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB1_000_000_000 lamports (use BigInt or n suffix)dashboard.onswig.com@swig-wallet/classic or @swig-wallet/kit (+ paymaster if needed)getSignInstructions to execute from the walletAlways wrap transactions in try/catch and handle common errors:
Always call swig.refetch() before building new instructions to ensure you have the latest on-chain state.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.