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 | mint, mintRef, name, symbol, uri, initialBuy, slippage, creatorFeeAddress, quoteMint and the V2 flags | A pump.fun launch, see create-token. The mint keypair signs. |
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.
{
"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(process.env.SECRET));
const conn = new Connection(process.env.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(os.environ["SECRET"])
conn = Client(os.environ["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.