TokenAnvil DevelopersGet API access

Developers / Resources

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

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.

More resources

Get API access ($10 one time)   Read the guide