Skip to main content
On Solana, SPL token transfers require the recipient to have an Associated Token Account (ATA) for that token. Gas Station does not pay for ATA creation unless you use Solana ATA sponsorship. Without sponsorship, you must create the ATA and pay its rent before a transfer can succeed. This quickstart walks you through a server-side script that creates a USDC ATA. A payer wallet pays the one-time SOL rent, and an owner wallet owns the ATA and receives USDC.
All transactions in this guide take place on Solana Devnet. No real funds are required beyond testnet SOL for rent and fees. You can adapt the code for mainnet by setting CLUSTER to 'mainnet-beta' and using mainnet USDC mint addresses.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v18+.
  • Created a payer wallet and obtained its keypair (private key).
  • Funded the payer wallet with at least ~0.00204 SOL on Solana Devnet to cover ATA rent-exempt minimum and transaction fees:
  • Obtained the owner wallet’s public key (base58 address). The owner can be a wallet you create or any recipient’s address.

Step 1: Set up the project

This step sets up your project environment and installs the required dependencies.

1.1. Create a new project

Create a new directory and initialize a new Node.js project with default settings:
Shell
mkdir create-usdc-ata-solana
cd create-usdc-ata-solana
npm init -y
npm pkg set type=module

1.2. Install dependencies

Install the required dependencies for Solana and SPL token interactions, and set the start script to run the TypeScript file with tsx:
Shell
npm install @solana/web3.js @solana/spl-token tsx
npm pkg set scripts.start="npx tsx --env-file=.env index.ts"
npm install --save-dev typescript @types/node

1.3. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
npx tsc --init
Then, update the tsconfig.json file:
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.4. Configure environment variables

Create a .env file with PAYER_PRIVATE_KEY (payer keypair as a JSON array) and OWNER_PUBLIC_KEY (base58 address of the wallet that will own the ATA):
Shell
echo "PAYER_PRIVATE_KEY=[1,2,3,...]" > .env
echo "OWNER_PUBLIC_KEY=YourOwnerBase58Address" >> .env
This use of a private key is simplified for demonstration purposes. In production, store and access your private key securely and never share it.
The PAYER_PRIVATE_KEY should be a JSON array of bytes representing your private key. You can export this from most Solana wallets.
Some wallets export Solana private keys as Base58 encoded strings. If you have a Base58 encoded private key, install bs58, save the following code as convert-key.ts, and run it with tsx to convert it to a JSON array:
Shell
npm install bs58
npx tsx convert-key.ts
TypeScript
import bs58 from "bs58";

const privateKeyBase58: string = "YOUR_BASE58_PRIVATE_KEY";

try {
  // Decode the Base58 string to a Uint8Array
  const privateKeyBytes: Uint8Array = bs58.decode(privateKeyBase58);

  // Convert the Uint8Array to a JSON array string
  const privateKeyJsonString: string = JSON.stringify(
    Array.from(privateKeyBytes),
  );

  console.log("JSON Array:", privateKeyJsonString);
} catch (error) {
  console.error(
    "Error converting key. Check if the Base58 key is valid.",
    error,
  );
}

Step 2: Create the script

This step creates the complete script. It adds imports and configuration, implements ATA creation, and runs the script.

2.1. Import dependencies

Create an index.ts file:
Shell
touch index.ts
Then, add the imports and configuration constants:
index.ts
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  sendAndConfirmTransaction,
  clusterApiUrl,
} from "@solana/web3.js";
import {
  createAssociatedTokenAccountIdempotentInstruction,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";

const CLUSTER: "mainnet-beta" | "devnet" = "devnet";

const USDC_MINT = {
  "mainnet-beta": new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
  devnet: new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"),
} as const;

2.2. Add the ATA creation logic

Add the following code to index.ts. This code defines the createUSDCata function and the main function.
The createUSDCata function:
  • Derives the ATA address.
  • Builds the idempotent ATA instruction.
  • Sends the transaction.
The main function:
  • Loads the payer keypair and owner address from .env.
  • Initializes the Solana connection.
  • Calls createUSDCata.
  • Prints the final ATA address.
The payer covers the ATA rent-exempt amount and transaction fees.
index.ts
async function createUSDCata(
  connection: Connection,
  payer: Keypair,
  owner: PublicKey,
): Promise<string> {
  // Select the correct USDC mint for the configured cluster.
  const mint = USDC_MINT[CLUSTER];

  // Derive the owner's Associated Token Account (ATA) for USDC.
  const ata = getAssociatedTokenAddressSync(mint, owner);

  // Create an idempotent ATA instruction.
  // If the ATA already exists, this instruction is a no-op.
  const ix = createAssociatedTokenAccountIdempotentInstruction(
    payer.publicKey,
    ata,
    owner,
    mint,
  );

  // Build and send the transaction. The payer signs and pays ATA rent plus fees.
  const tx = new Transaction().add(ix);
  await sendAndConfirmTransaction(connection, tx, [payer], {
    commitment: "confirmed",
  });

  // Print the ATA and a Solana Explorer link for verification.
  console.log("ATA created:", ata.toBase58());
  const explorerCluster =
    CLUSTER === "mainnet-beta" ? "" : `?cluster=${CLUSTER}`;
  console.log(
    `Explorer: https://explorer.solana.com/address/${ata.toBase58()}${explorerCluster}`,
  );

  return ata.toBase58();
}

async function main(): Promise<void> {
  const payerRaw = process.env.PAYER_PRIVATE_KEY;
  const ownerRaw = process.env.OWNER_PUBLIC_KEY;

  // Validate required environment variables.
  if (!payerRaw || !ownerRaw) {
    throw new Error(
      "Set PAYER_PRIVATE_KEY and OWNER_PUBLIC_KEY in .env (see Step 1.4)",
    );
  }

  // Parse keys and create an RPC connection.
  const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(payerRaw)));
  const owner = new PublicKey(ownerRaw);
  const connection = new Connection(clusterApiUrl(CLUSTER), "confirmed");

  // Create the ATA and print the final address.
  const ataAddress = await createUSDCata(connection, payer, owner);
  console.log("Ready to receive USDC at:", ataAddress);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

2.3. Run the script

Run the script with the following command:
Shell
npm start
You should see output similar to:
ATA created: DC85yuMEnGDTLpubqUC53BgmMeMjVvoqQyqopekUXffz
Explorer: https://explorer.solana.com/address/DC85yuMEnGDTLpubqUC53BgmMeMjVvoqQyqopekUXffz?cluster=devnet
Ready to receive USDC at: DC85yuMEnGDTLpubqUC53BgmMeMjVvoqQyqopekUXffz
Open the Solana Explorer link to verify the ATA onchain. Run the script again to confirm idempotent behavior: the transaction still succeeds and the ATA address is unchanged.