A Claude skill for subscribing to on-chain recurring payment plans using Dynamic MPC wallets.
- Creates a Dynamic WaaS Solana MPC wallet
- Checks SOL and USDC balance
- Swaps SOL → USDC via Dynamic's Swap API
- Sends SOL or any SPL token to another address (incl. sweep-all)
- Lists subscription plans for any merchant address
- Subscribes to a plan (initializes SubscriptionAuthority if needed)
- Cancels subscriptions
- Creates subscription plans (for merchants)
- Collects payments (for merchants)
- Sunsets / deletes plans and reclaims rent (for merchants)
# 1. Install dependencies
pnpm install
# 2. Configure credentials
cp .env.example .env
# Edit .env: add DYNAMIC_ENVIRONMENT_ID and DYNAMIC_AUTH_TOKEN
# 3. Just run a command — the wallet is created automatically on first use.
# (You can also create it explicitly with: pnpm subs setup)
pnpm subs balance
# 4. Fund the wallet with SOL for gas. Any on-chain command will print the
# wallet address and ask you to fund it if the balance is empty.
# Once it has SOL, mint USDC from it if needed:
pnpm subs swap 1.00
# 5. Browse plans
pnpm subs list-plans <merchant_solana_address>
# 6. Subscribe
pnpm subs subscribe <merchant_solana_address> 0First run auto-creates the MPC wallet and (if unfunded) prints a prompt to send SOL to the new address. There is no mainnet faucet — fund it from an exchange or another wallet, then re-run.
| Command | Description |
|---|---|
setup |
Create a Dynamic WaaS Solana wallet |
encrypt-shares |
Encrypt SOL_WALLET_KEY_SHARES at rest (needs SUBS_KEYSTORE_PASSPHRASE) |
secrets |
Move sensitive secrets from .env into the macOS Keychain & show where each lives |
balance |
Check SOL and USDC balance |
swap <usdc> |
Swap SOL → USDC |
send <recipient> <amount|all> [SOL|USDC|<mint>] |
Send SOL or an SPL token |
send-all <recipient> |
Sweep all SOL + USDC to one address |
create-plan <usdc> <period_hours> [metadata_uri] |
Publish a plan (merchant) |
list-plans <merchant> |
List plans for a merchant |
subscribe <merchant> <n> |
Subscribe to plan n (0-based) |
subs |
List your active subscriptions |
collect <plan_pda> <sub_pda> <delegator> <amount> |
Collect payment (merchant) |
cancel <plan_pda> <sub_pda> |
Cancel a subscription |
sunset-plan <plan_pda> [end_in_minutes] |
Retire a plan — no new subscriptions (merchant) |
delete-plan <plan_pda> |
Delete an expired plan & reclaim rent (merchant) |
The SKILL.md file is a Claude Code skill definition.
Install it and Claude can handle requests like:
"Claude, use my Dynamic wallet to subscribe to Crypto NFAs weekly"
This project uses pnpm (packageManager is pinned in package.json; run via
corepack enable or install pnpm directly). The committed .npmrc sets
always-auth=false so pnpm doesn't demand credentials for public registries.
The @dynamic-labs-wallet/* packages are served from a private jfrog registry.
Your global ~/.npmrc must carry the auth token for that host — and because
some tarballs live under a different repo path on the same host, the token must be
authorized at the host root, e.g.:
//<your-jfrog-host>/:_authToken=${YOUR_TOKEN}
(npm sends host-wide auth implicitly; pnpm matches by path, so the host-root entry
is required.) Never commit tokens — keep them in your global ~/.npmrc only.
- @solana/subscriptions — on-chain subscription program SDK
- @dynamic-labs-wallet/node-svm — Dynamic MPC signing for Solana
- @solana/kit — Solana transaction building
Credentials are loaded from .env and never logged or transmitted beyond the Dynamic and Solana APIs.
Run pnpm subs without arguments to see the usage summary.
SOL_WALLET_KEY_SHARES is the customer-side half of MPC signing. Combined with
your DYNAMIC_AUTH_TOKEN (and WALLET_BACKUP_PASSWORD), it confers signing
authority — treat it like a private key. It is encrypted, not hashed, because the
share must be recoverable to sign.
Where things are stored
| Value | Sensitivity | Stored in |
|---|---|---|
SOL_WALLET_ADDRESS, DYNAMIC_ENVIRONMENT_ID, TOKEN_MINT |
Non-sensitive | .env |
SOL_WALLET_KEY_SHARES, DYNAMIC_AUTH_TOKEN, WALLET_BACKUP_PASSWORD |
Sensitive | macOS Keychain (falls back to .env off-macOS) |
SUBS_KEYSTORE_PASSPHRASE |
Sensitive | nowhere — per session |
- Encryption at rest: the key share is wrapped with AES-256-GCM (unique salt + IV,
tamper-evident), key derived via PBKDF2-SHA256 (600k iterations) from the keystore
passphrase. Envelope:
enc:v1:<salt>:<iv>:<authTag>:<ciphertext>. - Per-session passphrase:
SUBS_KEYSTORE_PASSPHRASEis never stored.setupprompts you to set one; signing reads it from the env var (export it for the session) or prompts interactively. Keeping it out of the Keychain means a Keychain compromise alone can't decrypt the share. pnpm subs secretsmoves any secrets sitting in.envinto the Keychain.pnpm subs encrypt-sharesencrypts the share if you set the wallet up without a passphrase.- Back up
WALLET_BACKUP_PASSWORDoff-machine (e.g. a password manager): it's the recovery key for the Dynamic-side backup. Losing both it and the local share = no recovery.
Threat model — what this does and does not protect
- ✅ A leaked
.envcontains no signing material — secrets are in the Keychain. - ✅ A Keychain compromise alone can't decrypt the share — the passphrase is supplied per session, not stored.
- ❌ Does not protect against code running in your session: to sign, the share is
decrypted into process memory and the passphrase is in the runtime env. Don't paste
SUBS_KEYSTORE_PASSPHRASEinto chat or export it in an unattended agent session. - 🔐 Real defense-in-depth is the 2-of-2 MPC threshold: the client share alone
cannot sign — it also needs the Dynamic server share via
DYNAMIC_AUTH_TOKEN.
Also persist the full
walletMetadataobject (includingexternalServerKeySharesBackupInfo) — it's required to sign/export and is not recoverable fromfetchWalletMetadata, which returns identity only. This skill re-fetches it per environment viagetWallets(); a production service should cache it durably.