Why the API returns an unsigned transaction, and how to sign it safely
The API builds transactions; it never signs them. That is the whole security model: even a compromised API key cannot move your funds, because the only thing the key can do is ask for a transaction that your wallet then chooses to sign.
What "unsigned" means
A Solana transaction is a message (accounts, instructions, a recent blockhash) plus signatures. The API returns the message with empty signature slots. You add your signature and submit it. Because the message is fixed, anything the transaction does is visible before you sign.
Inspect before you sign
Automation should check what it is about to sign. This check refuses any transaction that calls a program outside the small set a token creation needs, or that uses a different fee payer:
import { Transaction } from "@solana/web3.js";
const tx = Transaction.from(Buffer.from(built.transaction, "base64"));
const allowed = new Set([
"ComputeBudget111111111111111111111111111111", // optional priority fee settings
"11111111111111111111111111111111", // System program: create the mint account, pay the fee
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token program
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token Account program
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", // Metaplex Token Metadata program
]);
for (const ix of tx.instructions) {
if (!allowed.has(ix.programId.toBase58())) throw new Error("Unexpected program: " + ix.programId.toBase58());
}
if (tx.feePayer.toBase58() !== wallet.publicKey.toBase58()) throw new Error("Unexpected fee payer");
You can go further: decode the instructions and confirm the only transfer of SOL out of your wallet goes to the treasury address returned in fee.treasury for the amount in fee.lamports, plus rent for new accounts.
The signing window
Every transaction embeds a recent blockhash, which expires after roughly a minute and a half. The API tells you expiresInSeconds and lastValidBlockHeight. If the window passes, build a new transaction; do not try to reuse the old one.
Protect the wallet
- Use a dedicated wallet for automation and fund it with only what the next few creations need.
- Keep the secret key in your platform's secret store, never in source control or a client app.
- Keep the API key on the server as well. It is shown once and stored only as a keyed hash on our side, so rotate it in the portal if it may have leaked.
- Browsers should never hold either secret. If a user creates a token from a web page, have their own wallet sign through a wallet adapter.
Why not let the server sign?
Because then the service would hold keys to a wallet with funds, and every customer would have to trust it. Non-custodial design removes that trust requirement and also removes our ability to lose your funds. The same reasoning is why the mint address is derived rather than generated: there is no second keypair to store or leak.
Related: the complete creation flow.