Creating a token on Solana requires three steps: create a mint account, create a holding account for the recipient, and mint tokens to it. This guide covers both SPL Token and Token-2022 paths using the Protochain Token Program Service.
Choose your token program
Both token programs use the same Protochain API shape — you choose the program once, at mint creation, and it cannot be changed. See Token Programs for the full trade-off comparison.
| SPL Token | Token-2022 |
|---|
| Metadata | Metaplex PDA (external) | On-chain extension (native) |
| Extensions | Not supported | Transfer fees, interest-bearing, etc. |
| Compatibility | Maximum ecosystem support | Newer wallets/DEXs only |
If you need extensions or native on-chain metadata, use Token-2022. If you need maximum wallet compatibility, use SPL Token.
Prerequisites
Create the mint
Call CreateToken2022Mint or CreateSPLTokenMint depending on your token program choice. Both return a list of instructions — include all of them in the transaction in order (the service handles ordering).
Token-2022 mint
resp, err := tokenClient.CreateToken2022Mint(ctx, &token_v1.CreateToken2022MintRequest{
PayerPubKey: "PayerPubKey1111111111111111111111111111111",
MintPubKey: "MintPubKey111111111111111111111111111111111",
MintAuthorityPubKey: "PayerPubKey1111111111111111111111111111111",
Decimals: 6, // like USDC; use 0 for NFTs
Extensions: []*token_v1.Token2022Extension{}, // add Token2022Extension values here for transfer fees, etc.
})
if err != nil {
log.Fatal(err)
}
// Collect all instructions — the response may include multiple
mintInstructions := resp.Instructions
SPL Token mint
resp, err := tokenClient.CreateSPLTokenMint(ctx, &token_v1.CreateSPLTokenMintRequest{
PayerPubKey: "PayerPubKey1111111111111111111111111111111",
MintPubKey: "MintPubKey111111111111111111111111111111111",
MintAuthorityPubKey: "PayerPubKey1111111111111111111111111111111",
Decimals: 6, // like USDC; use 0 for NFTs
// Metadata: &token_v1.MetaplexTokenMetadata{
// Name: "My Token",
// Symbol: "MTK",
// Uri: "https://example.com/metadata.json",
// },
})
if err != nil {
log.Fatal(err)
}
// Collect all instructions — the response may include multiple
mintInstructions := resp.Instructions
Create a holding account
Before minting, the recipient needs a token holding account (Associated Token Account) for this mint. Call CreateToken2022HoldingAccount or CreateSPLTokenHoldingAccount — use the same token program as the mint.
Pass owner_pub_key as the recipient’s system wallet address, not a pre-computed ATA address. Protochain derives the ATA automatically.
Token-2022 holding account
holdResp, err := tokenClient.CreateToken2022HoldingAccount(ctx, &token_v1.CreateToken2022HoldingAccountRequest{
PayerPubKey: "PayerPubKey1111111111111111111111111111111",
OwnerPubKey: "RecipientPubKey11111111111111111111111111111",
MintPubKey: "MintPubKey111111111111111111111111111111111",
Extensions: []*token_v1.Token2022HoldingAccountExtension{}, // add holding account extensions here
})
if err != nil {
log.Fatal(err)
}
holdInstructions := holdResp.Instructions
SPL Token holding account
holdResp, err := tokenClient.CreateSPLTokenHoldingAccount(ctx, &token_v1.CreateSPLTokenHoldingAccountRequest{
PayerPubKey: "PayerPubKey1111111111111111111111111111111",
OwnerPubKey: "RecipientPubKey11111111111111111111111111111",
MintPubKey: "MintPubKey111111111111111111111111111111111",
})
if err != nil {
log.Fatal(err)
}
holdInstructions := holdResp.Instructions
Compile and submit the mint transaction
Collect all instructions from the mint creation and holding account responses, assemble them into a single transaction, compile with CompileTransaction, sign with both the fee payer key and the mint keypair, then submit.
The mint account keypair must be included as a signer in SignTransaction — it is not just a public key reference. Both the payer and the mint keypair are required signers.
// Assemble all instructions into one transaction
tx := &transaction_v1.Transaction{State: transaction_v1.TransactionState_TRANSACTION_STATE_DRAFT}
for _, instr := range mintInstructions {
tx.Instructions = append(tx.Instructions, instr)
}
for _, instr := range holdInstructions {
tx.Instructions = append(tx.Instructions, instr)
}
// Compile
compileResp, err := txClient.CompileTransaction(ctx, &transaction_v1.CompileTransactionRequest{
Transaction: tx,
FeePayer: "PayerPubKey1111111111111111111111111111111",
})
if err != nil {
log.Fatal(err)
}
// Sign with both payer and mint keypair
signResp, err := txClient.SignTransaction(ctx, &transaction_v1.SignTransactionRequest{
Transaction: compileResp.Transaction,
PrivateKeys: []string{"payerPrivateKey...", "mintPrivateKey..."},
})
if err != nil {
log.Fatal(err)
}
// Submit
submitResp, err := txClient.SubmitTransaction(ctx, &transaction_v1.SubmitTransactionRequest{
Transaction: signResp.Transaction,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("mint tx signature: %s\n", submitResp.Signature)
Mint tokens
With the mint account created, call Mint to issue tokens. Mint is token-program agnostic — Protochain reads the mint on-chain to determine the token program automatically.
Pass destination_owner_pub_key as the recipient’s system wallet address, not the ATA address. The ATA is derived automatically.
mintResp, err := tokenClient.Mint(ctx, &token_v1.MintRequest{
MintPubKey: "MintPubKey111111111111111111111111111111111",
DestinationOwnerPubKey: "RecipientPubKey11111111111111111111111111111",
Amount: "1000.0", // human-readable; 1000 tokens at 6 decimals = 1,000,000,000 base units
})
if err != nil {
log.Fatal(err)
}
Add the returned instruction to a transaction, then compile, sign (mint authority only this time), and submit using the same compile → sign → submit pattern from the previous section.
Next steps