Multi-action transactions
One transaction, several things in it: buy on one launchpad and sell on another, launch a coin and buy it, buy and move the tokens somewhere else, claim creator fees and burn. Everything in the list lands together or not at all.
POST https://sol.shrine.trade/api/local-actions
{
"publicKey": "<fee payer, and the wallet for actions without their own>",
"actions": [
{ "action": "buy", "mint": "<mint>", "amount": 0.05, "slippage": 15 },
{ "action": "transfer", "to": "<other wallet>", "mint": "<mint>", "amount": "50%" },
{ "action": "burn", "mint": "<mint>", "amount": "100%" }
],
"priorityFee": 0.0005,
"jitoTip": 0
}
The answer is one unsigned transaction plus the list of keys that must sign it:
{
"tx": "<base64 unsigned transaction>",
"blockhash": "…",
"signers": ["<fee payer>", "<other signing wallets or mint keypairs, in order>"],
"feeLamports": 125000,
"sizeBytes": 1088,
"actions": [
{ "action": "buy", "wallet": "…", "mint": "…", "amount": 0.05, "feeLamports": 125000 },
{ "action": "transfer", "wallet": "…", "mint": "…", "amount": 12345.6, "feeLamports": 0 },
{ "action": "burn", "wallet": "…", "mint": "…", "amount": 12345.6, "feeLamports": 0 }
]
}
Sign with every key in signers and send it through your own RPC, exactly like a single trade.
Actions
Every action takes an optional publicKey. Without one it runs as the top-level wallet; with one, that wallet signs too and appears in signers.
action | Fields | Notes |
|---|---|---|
buy | mint, amount (SOL, or the coin's quote), slippage, pool, poolAddress | Any launchpad the trade endpoint routes. 0.25% fee. |
sell | mint, amount (tokens or "100%"), slippage, pool, poolAddress | 0.25% fee. |
create | name, symbol, then either uri or imageUrl (+ description, twitter, telegram, website), optional mint, mintRef, initialBuy, slippage, creatorFeeAddress, quoteMint and the V2 flags | A pump.fun launch, see create-token. Without uri the metadata is pinned for you; without mint you get an address ending in pump that we sign for. |
transfer | to, amount, mint (omit for SOL) | Creates the recipient's token account if needed. Free. |
burn | mint, amount | Burning all of it closes the account and refunds its rent. Free. |
claimCreatorFees | Same as claim-creator-fees. Free. | |
wrap | amount (SOL) | SOL into the wallet's WSOL account. Free. |
unwrap | Closes the WSOL account, every lamport comes back. Free. |
amount on a sell, transfer or burn may be a number of tokens or a percentage of what the wallet holds. "100%", "50%" and "all" are all understood.
Launch and buy in one go
A coin created in the transaction has no address yet when you write the request, so give the create a mintRef and refer to it as "$name" afterwards. Generate the mint keypair yourself and pass its public key as mint; it signs the transaction along with the wallet. Or leave mint out: the coin gets an address ending in pump, the transaction comes back already signed by it, and mintSecretKeys in the answer lists each provided mint with its secret. The same holds inside a bundle.
{
"publicKey": "<dev wallet>",
"actions": [
{ "action": "create", "mintRef": "coin", "mint": "<mint keypair public key>", "name": "My Coin", "symbol": "COIN", "uri": "https://…/metadata.json", "initialBuy": 0.5 },
{ "action": "buy", "mint": "$coin", "amount": 0.2, "slippage": 30 }
]
}
Buys on a coin born in the same transaction are quoted from the curve's launch constants, moved along by the SOL already put in before them, so give them a generous slippage.
How much fits
Solana caps a transaction at 1232 bytes. With the shared lookup table that is about four swaps on one launchpad, fewer across launchpads, and a launch with a dev buy and one more buy. When a set does not fit, the answer is a 400 naming the size; split it across a bundle, where mintRef still resolves from one transaction to the next.
Copy this
- JavaScript
- Python
import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";
const wallet = Keypair.fromSecretKey(bs58.decode("<your wallet private key, base58>"));
const conn = new Connection("<your RPC URL>", "confirmed");
const res = await fetch("https://sol.shrine.trade/api/local-actions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
publicKey: wallet.publicKey.toBase58(),
actions: [
{ action: "buy", mint: "<mint>", amount: 0.01, slippage: 15 },
{ action: "transfer", to: "<friend>", mint: "<mint>", amount: "50%" },
],
}),
});
const { tx, signers } = await res.json();
const txObj = VersionedTransaction.deserialize(Buffer.from(tx, "base64"));
txObj.sign([wallet]); // every key listed in `signers`
console.log(await conn.sendRawTransaction(txObj.serialize()));
import base64, os, requests
from solders.keypair import Keypair
from solders.transaction import VersionedTransaction
from solana.rpc.api import Client
wallet = Keypair.from_base58_string("<your wallet private key, base58>")
conn = Client("<your RPC URL>")
r = requests.post("https://sol.shrine.trade/api/local-actions", json={
"publicKey": str(wallet.pubkey()),
"actions": [
{"action": "buy", "mint": "<mint>", "amount": 0.01, "slippage": 15},
{"action": "transfer", "to": "<friend>", "mint": "<mint>", "amount": "50%"},
],
}).json()
raw = VersionedTransaction.from_bytes(base64.b64decode(r["tx"]))
signed = VersionedTransaction(raw.message, [wallet]) # every key in r["signers"]
print(conn.send_raw_transaction(bytes(signed)).value)
Notes
- Atomic. One instruction failing reverts the whole transaction: nothing partial ever lands.
- Priority fee is one number for the whole transaction. The compute budget is sized from the actions inside.
- Fees. 0.25% on each buy and sell, taken in SOL inside the same transaction and merged into one transfer per wallet. Everything else is free.
- Keyless and unsigned, like every trade endpoint. No key, no account; nothing is signed or sent by us.