Complete Token Launch Flow
JavaScript/TypeScript (Web)
A complete example using modern web technologies:Copy
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import bs58 from 'bs58';
interface TokenData {
name: string;
symbol: string;
description?: string;
image?: string;
website?: string;
twitter?: string;
caEnding?: string;
quoteToken?: string;
}
class TokenLaunchClient {
private baseURL = 'https://api.zcombinator.io';
private requestCount = 0;
private windowStart = Date.now();
constructor(private wallet: any) {}
private async makeRequest(endpoint: string, options: RequestInit) {
// Simple rate limiting
const now = Date.now();
if (now - this.windowStart >= 2 * 60 * 1000) {
this.requestCount = 0;
this.windowStart = now;
}
if (this.requestCount >= 4) {
const waitTime = 2 * 60 * 1000 - (now - this.windowStart);
throw new Error(`Rate limited. Wait ${Math.ceil(waitTime / 1000)} seconds.`);
}
const response = await fetch(`${this.baseURL}${endpoint}`, options);
this.requestCount++;
if (response.status === 429) {
throw new Error('Rate limited by server. Please wait 2 minutes.');
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'API request failed');
}
return data;
}
async launchToken(tokenData: TokenData): Promise<any> {
if (!this.wallet.publicKey) {
throw new Error('Wallet not connected');
}
// Step 1: Validate inputs
this.validateTokenData(tokenData);
// Step 2: Create unsigned transaction
console.log('Creating launch transaction...');
const launchResult = await this.makeRequest('/launch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...tokenData,
payerPublicKey: this.wallet.publicKey.toString(),
quoteToken: tokenData.quoteToken || "SOL" // Defaults to SOL if not specified
})
});
// Step 3: Sign transaction
console.log('Signing transaction...');
const transaction = Transaction.from(bs58.decode(launchResult.transaction));
const signedTransaction = await this.wallet.signTransaction(transaction);
// Step 4: Confirm launch
console.log('Confirming launch...');
const confirmResult = await this.makeRequest('/confirm-launch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signedTransaction: bs58.encode(signedTransaction.serialize()),
baseMint: launchResult.baseMint,
metadataUrl: launchResult.metadataUrl,
name: tokenData.name,
symbol: tokenData.symbol,
payerPublicKey: this.wallet.publicKey.toString()
})
});
console.log('Token launched successfully!', confirmResult.transactionSignature);
return confirmResult;
}
async checkClaimEligibility(tokenAddress: string, walletAddress: string) {
const result = await this.makeRequest(
`/claims/${tokenAddress}?wallet=${walletAddress}`,
{ method: 'GET' }
);
return result;
}
async claimTokens(tokenAddress: string, amount: string): Promise<any> {
if (!this.wallet.publicKey) {
throw new Error('Wallet not connected');
}
// Step 1: Check eligibility
const eligibility = await this.checkClaimEligibility(
tokenAddress,
this.wallet.publicKey.toString()
);
if (!eligibility.canClaimNow) {
throw new Error(`Cannot claim now. Next claim: ${eligibility.nextInflationTime}`);
}
if (BigInt(amount) > BigInt(eligibility.availableToClaim)) {
throw new Error(`Amount ${amount} exceeds available ${eligibility.availableToClaim}`);
}
// Step 2: Create claim transaction
console.log('Creating claim transaction...');
const mintResult = await this.makeRequest('/claims/mint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tokenAddress,
userWallet: this.wallet.publicKey.toString(),
claimAmount: amount
})
});
// Step 3: Sign transaction
console.log('Signing claim transaction...');
const transaction = Transaction.from(bs58.decode(mintResult.transaction));
const signedTransaction = await this.wallet.signTransaction(transaction);
// Step 4: Confirm claim
console.log('Confirming claim...');
const confirmResult = await this.makeRequest('/claims/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signedTransaction: bs58.encode(signedTransaction.serialize()),
transactionKey: mintResult.transactionKey
})
});
console.log('Tokens claimed successfully!', confirmResult.transactionSignature);
return confirmResult;
}
async verifyToken(address: string) {
return this.makeRequest(`/verify-token/${address}`, { method: 'GET' });
}
async getHealth() {
return this.makeRequest('/health', { method: 'GET' });
}
private validateTokenData(data: TokenData) {
if (!data.name?.trim()) throw new Error('Token name is required');
if (!data.symbol?.trim()) throw new Error('Token symbol is required');
if (data.caEnding && data.caEnding.length > 3) {
throw new Error('CA ending must be 3 characters or less');
}
if (data.caEnding && /[0OIl]/.test(data.caEnding)) {
throw new Error('CA ending contains invalid Base58 characters');
}
}
}
// Usage example
async function example() {
// Assume wallet is connected (Phantom, Solflare, etc.)
const wallet = window.solana;
await wallet.connect();
const client = new TokenLaunchClient(wallet);
try {
// Check API health
const health = await client.getHealth();
console.log('API Health:', health);
// Launch a token
const launchResult = await client.launchToken({
name: "My Awesome Token",
symbol: "MAT",
description: "A demonstration token",
image: "https://example.com/token-logo.png",
website: "https://mytoken.com",
twitter: "https://twitter.com/mytoken",
caEnding: "MAT",
quoteToken: "SOL" // Optional: "SOL" or "ZC"
});
console.log('Launched token:', launchResult.baseMint);
// Verify the token was created
const verification = await client.verifyToken(launchResult.baseMint);
console.log('Token exists:', verification.exists);
// Check claim eligibility
const eligibility = await client.checkClaimEligibility(
launchResult.baseMint,
wallet.publicKey.toString()
);
console.log('Can claim:', eligibility.canClaimNow);
// Claim tokens if eligible
if (eligibility.canClaimNow && BigInt(eligibility.availableToClaim) > 0) {
const claimResult = await client.claimTokens(
launchResult.baseMint,
eligibility.availableToClaim
);
console.log('Claimed tokens:', claimResult.transactionSignature);
}
} catch (error) {
console.error('Error:', error.message);
}
}
Python (Backend)
Server-side integration for automated systems:Copy
import requests
import time
import base58
from solders.keypair import Keypair
from solders.transaction import Transaction
from typing import Dict, Any, Optional
class TokenLaunchClient:
def __init__(self, wallet_keypair: Keypair):
self.base_url = 'https://api.zcombinator.io'
self.wallet_keypair = wallet_keypair
self.session = requests.Session()
self.request_count = 0
self.window_start = time.time()
def _make_request(self, endpoint: str, method: str = 'GET', data: Optional[Dict] = None) -> Dict[str, Any]:
"""Make API request with rate limiting"""
# Simple rate limiting
now = time.time()
if now - self.window_start >= 120: # 2 minutes
self.request_count = 0
self.window_start = now
if self.request_count >= 4:
wait_time = 120 - (now - self.window_start)
raise Exception(f"Rate limited. Wait {wait_time:.0f} seconds.")
url = f"{self.base_url}{endpoint}"
try:
if method == 'POST':
response = self.session.post(url, json=data, timeout=30)
else:
response = self.session.get(url, timeout=30)
self.request_count += 1
if response.status_code == 429:
raise Exception("Rate limited by server. Please wait 2 minutes.")
response.raise_for_status()
return response.json()
except requests.RequestException as e:
raise Exception(f"API request failed: {str(e)}")
def launch_token(self, token_data: Dict[str, str]) -> Dict[str, Any]:
"""Launch a new token"""
print("Creating launch transaction...")
# Step 1: Create unsigned transaction
launch_result = self._make_request('/launch', 'POST', {
**token_data,
'payerPublicKey': str(self.wallet_keypair.pubkey()),
'quoteToken': token_data.get('quoteToken', 'SOL') # Optional: 'SOL' or 'ZC'
})
# Step 2: Sign transaction
print("Signing transaction...")
transaction_bytes = base58.b58decode(launch_result['transaction'])
transaction = Transaction.from_bytes(transaction_bytes)
transaction.sign([self.wallet_keypair])
# Step 3: Confirm launch
print("Confirming launch...")
confirm_result = self._make_request('/confirm-launch', 'POST', {
'signedTransaction': base58.b58encode(bytes(transaction)).decode(),
'baseMint': launch_result['baseMint'],
'metadataUrl': launch_result['metadataUrl'],
'name': token_data['name'],
'symbol': token_data['symbol'],
'payerPublicKey': str(self.wallet_keypair.pubkey())
})
print(f"Token launched successfully! Transaction: {confirm_result['transactionSignature']}")
return confirm_result
def check_claim_eligibility(self, token_address: str, wallet_address: str) -> Dict[str, Any]:
"""Check claim eligibility for a wallet"""
return self._make_request(f'/claims/{token_address}?wallet={wallet_address}')
def claim_tokens(self, token_address: str, amount: str) -> Dict[str, Any]:
"""Claim available tokens"""
wallet_address = str(self.wallet_keypair.pubkey())
# Step 1: Check eligibility
eligibility = self.check_claim_eligibility(token_address, wallet_address)
if not eligibility['canClaimNow']:
raise Exception(f"Cannot claim now. Next claim: {eligibility['nextInflationTime']}")
if int(amount) > int(eligibility['availableToClaim']):
raise Exception(f"Amount {amount} exceeds available {eligibility['availableToClaim']}")
# Step 2: Create claim transaction
print("Creating claim transaction...")
mint_result = self._make_request('/claims/mint', 'POST', {
'tokenAddress': token_address,
'userWallet': wallet_address,
'claimAmount': amount
})
# Step 3: Sign transaction
print("Signing claim transaction...")
transaction_bytes = base58.b58decode(mint_result['transaction'])
transaction = Transaction.from_bytes(transaction_bytes)
transaction.sign([self.wallet_keypair])
# Step 4: Confirm claim
print("Confirming claim...")
confirm_result = self._make_request('/claims/confirm', 'POST', {
'signedTransaction': base58.b58encode(bytes(transaction)).decode(),
'transactionKey': mint_result['transactionKey']
})
print(f"Tokens claimed successfully! Transaction: {confirm_result['transactionSignature']}")
return confirm_result
def verify_token(self, address: str) -> Dict[str, Any]:
"""Verify if a token exists"""
return self._make_request(f'/verify-token/{address}')
def get_health(self) -> Dict[str, Any]:
"""Check API health"""
return self._make_request('/health')
# Usage example
def main():
# Load your wallet keypair
# keypair = Keypair.from_bytes(your_private_key_bytes)
keypair = Keypair() # Generate new keypair for demo
client = TokenLaunchClient(keypair)
try:
# Check API health
health = client.get_health()
print("API Health:", health)
# Launch a token
token_data = {
'name': 'My Python Token',
'symbol': 'PYT',
'description': 'A token launched from Python',
'caEnding': 'PYT',
'quoteToken': 'SOL' # Optional: 'SOL' or 'ZC'
}
launch_result = client.launch_token(token_data)
token_address = launch_result['baseMint']
# Verify token was created
verification = client.verify_token(token_address)
print(f"Token exists: {verification['exists']}")
# Check claim eligibility
eligibility = client.check_claim_eligibility(
token_address,
str(keypair.pubkey())
)
print(f"Can claim: {eligibility['canClaimNow']}")
print(f"Available to claim: {eligibility['availableToClaim']}")
# Claim tokens if eligible
if eligibility['canClaimNow'] and int(eligibility['availableToClaim']) > 0:
claim_result = client.claim_tokens(
token_address,
eligibility['availableToClaim']
)
print(f"Claimed tokens: {claim_result['transactionSignature']}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Go (Backend)
High-performance server integration:Copy
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type TokenLaunchClient struct {
BaseURL string
HTTPClient *http.Client
RequestCount int
WindowStart time.Time
}
type TokenData struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Description string `json:"description,omitempty"`
Image string `json:"image,omitempty"`
Website string `json:"website,omitempty"`
Twitter string `json:"twitter,omitempty"`
CAEnding string `json:"caEnding,omitempty"`
PayerPubkey string `json:"payerPublicKey"`
QuoteToken string `json:"quoteToken,omitempty"`
}
type LaunchResult struct {
Success bool `json:"success"`
Transaction string `json:"transaction"`
BaseMint string `json:"baseMint"`
MetadataURL string `json:"metadataUrl"`
Message string `json:"message"`
}
type ClaimEligibility struct {
WalletAddress string `json:"walletAddress"`
TokenAddress string `json:"tokenAddress"`
TotalClaimed string `json:"totalClaimed"`
AvailableToClaim string `json:"availableToClaim"`
MaxClaimableNow string `json:"maxClaimableNow"`
TokensPerPeriod string `json:"tokensPerPeriod"`
InflationPeriods int `json:"inflationPeriods"`
TokenLaunchTime time.Time `json:"tokenLaunchTime"`
NextInflationTime time.Time `json:"nextInflationTime"`
CanClaimNow bool `json:"canClaimNow"`
TimeUntilNextClaim int64 `json:"timeUntilNextClaim"`
}
func NewTokenLaunchClient() *TokenLaunchClient {
return &TokenLaunchClient{
BaseURL: "https://api.zcombinator.io",
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
WindowStart: time.Now(),
}
}
func (c *TokenLaunchClient) makeRequest(method, endpoint string, body interface{}) ([]byte, error) {
// Rate limiting
now := time.Now()
if now.Sub(c.WindowStart) >= 2*time.Minute {
c.RequestCount = 0
c.WindowStart = now
}
if c.RequestCount >= 4 {
waitTime := 2*time.Minute - now.Sub(c.WindowStart)
return nil, fmt.Errorf("rate limited. wait %v", waitTime)
}
var reqBody io.Reader
if body != nil {
jsonData, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewBuffer(jsonData)
}
req, err := http.NewRequest(method, c.BaseURL+endpoint, reqBody)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
c.RequestCount++
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == 429 {
return nil, fmt.Errorf("rate limited by server")
}
if resp.StatusCode >= 400 {
var errorResp map[string]interface{}
json.Unmarshal(respBody, &errorResp)
if msg, ok := errorResp["error"].(string); ok {
return nil, fmt.Errorf("API error: %s", msg)
}
return nil, fmt.Errorf("API error: status %d", resp.StatusCode)
}
return respBody, nil
}
func (c *TokenLaunchClient) CreateLaunchTransaction(tokenData TokenData) (*LaunchResult, error) {
respBody, err := c.makeRequest("POST", "/launch", tokenData)
if err != nil {
return nil, err
}
var result LaunchResult
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (c *TokenLaunchClient) GetClaimEligibility(tokenAddress, walletAddress string) (*ClaimEligibility, error) {
endpoint := fmt.Sprintf("/claims/%s?wallet=%s", tokenAddress, walletAddress)
respBody, err := c.makeRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
var result ClaimEligibility
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (c *TokenLaunchClient) VerifyToken(address string) (map[string]interface{}, error) {
endpoint := fmt.Sprintf("/verify-token/%s", address)
respBody, err := c.makeRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
var result map[string]interface{}
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return result, nil
}
func (c *TokenLaunchClient) GetHealth() (map[string]interface{}, error) {
respBody, err := c.makeRequest("GET", "/health", nil)
if err != nil {
return nil, err
}
var result map[string]interface{}
err = json.Unmarshal(respBody, &result)
if err != nil {
return nil, err
}
return result, nil
}
func main() {
client := NewTokenLaunchClient()
// Check API health
health, err := client.GetHealth()
if err != nil {
fmt.Printf("Health check failed: %v\n", err)
return
}
fmt.Printf("API Health: %+v\n", health)
// Create launch transaction
tokenData := TokenData{
Name: "Go Token",
Symbol: "GOTKN",
Description: "A token launched from Go",
CAEnding: "GO",
PayerPubkey: "YourWalletPublicKeyHere",
QuoteToken: "SOL", // Optional: "SOL" or "ZC"
}
result, err := client.CreateLaunchTransaction(tokenData)
if err != nil {
fmt.Printf("Launch failed: %v\n", err)
return
}
fmt.Printf("Launch transaction created: %s\n", result.BaseMint)
fmt.Printf("Transaction to sign: %s\n", result.Transaction)
// Note: In a real implementation, you would:
// 1. Deserialize the transaction
// 2. Sign it with your wallet
// 3. Call /confirm-launch with the signed transaction
}
Testing and Development
Local Testing Setup
Copy
// test-client.js
const { TokenLaunchClient } = require('./token-launch-client');
class TestClient extends TokenLaunchClient {
constructor() {
super(null); // No wallet for testing
this.baseURL = 'https://api.zcombinator.io'; // Use real API for integration tests
}
async testHealthEndpoint() {
try {
const health = await this.getHealth();
console.log('✅ Health endpoint working:', health.status);
return true;
} catch (error) {
console.error('❌ Health endpoint failed:', error.message);
return false;
}
}
async testTokenVerification() {
try {
// Test with known token (USDC)
const result = await this.verifyToken('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
console.log('✅ Token verification working:', result.exists);
return true;
} catch (error) {
console.error('❌ Token verification failed:', error.message);
return false;
}
}
async testRateLimiting() {
console.log('Testing rate limiting...');
let successCount = 0;
let rateLimited = false;
for (let i = 0; i < 6; i++) {
try {
await this.getHealth();
successCount++;
console.log(`Request ${i + 1}: Success`);
} catch (error) {
if (error.message.includes('rate limit')) {
rateLimited = true;
console.log(`Request ${i + 1}: Rate limited (expected)`);
break;
} else {
console.error(`Request ${i + 1}: Unexpected error:`, error.message);
}
}
// Small delay between requests
await new Promise(resolve => setTimeout(resolve, 100));
}
if (rateLimited && successCount >= 4) {
console.log('✅ Rate limiting working correctly');
return true;
} else {
console.log('❌ Rate limiting not working as expected');
return false;
}
}
async runAllTests() {
console.log('Running API integration tests...\n');
const tests = [
{ name: 'Health Endpoint', fn: () => this.testHealthEndpoint() },
{ name: 'Token Verification', fn: () => this.testTokenVerification() },
{ name: 'Rate Limiting', fn: () => this.testRateLimiting() }
];
let passed = 0;
let total = tests.length;
for (const test of tests) {
console.log(`Running: ${test.name}`);
const result = await test.fn();
if (result) passed++;
console.log('');
}
console.log(`\nTest Results: ${passed}/${total} passed`);
if (passed === total) {
console.log('🎉 All tests passed!');
} else {
console.log('❌ Some tests failed. Check your API configuration.');
}
}
}
// Run tests
if (require.main === module) {
const testClient = new TestClient();
testClient.runAllTests().catch(console.error);
}
module.exports = { TestClient };
Error Handling Examples
Copy
class RobustTokenClient {
constructor(wallet) {
this.wallet = wallet;
this.baseURL = 'https://api.zcombinator.io';
this.maxRetries = 3;
}
async makeRequestWithRetry(endpoint, options, retryCount = 0) {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, options);
if (response.status === 429) {
// Rate limited - wait and retry
if (retryCount < this.maxRetries) {
const delay = Math.min(1000 * (2 ** retryCount), 30000);
console.log(`Rate limited. Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.makeRequestWithRetry(endpoint, options, retryCount + 1);
}
throw new Error('Rate limit exceeded. Please try again later.');
}
if (response.status >= 500) {
// Server error - retry with exponential backoff
if (retryCount < this.maxRetries) {
const delay = 1000 * (2 ** retryCount);
console.log(`Server error. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.makeRequestWithRetry(endpoint, options, retryCount + 1);
}
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
return data;
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
// Network error
if (retryCount < this.maxRetries) {
const delay = 1000 * (2 ** retryCount);
console.log(`Network error. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.makeRequestWithRetry(endpoint, options, retryCount + 1);
}
throw new Error('Network connection failed. Please check your internet connection.');
}
throw error;
}
}
async launchTokenSafely(tokenData) {
try {
// Validate inputs first
this.validateTokenData(tokenData);
// Check wallet connection
if (!this.wallet.publicKey) {
throw new Error('Please connect your wallet first.');
}
// Check API health
try {
await this.makeRequestWithRetry('/health', { method: 'GET' });
} catch (error) {
throw new Error('API is currently unavailable. Please try again later.');
}
// Proceed with launch...
const launchResult = await this.makeRequestWithRetry('/launch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...tokenData,
payerPublicKey: this.wallet.publicKey.toString(),
quoteToken: tokenData.quoteToken || "SOL" // Optional: "SOL" or "ZC"
})
});
return launchResult;
} catch (error) {
// Categorize and handle different types of errors
if (error.message.includes('Missing required fields')) {
throw new Error('Please fill in all required fields (name, symbol).');
} else if (error.message.includes('CA ending')) {
throw new Error('Custom address ending must be 3 characters or less with valid characters.');
} else if (error.message.includes('rate limit')) {
throw new Error('Too many requests. Please wait a moment before trying again.');
} else if (error.message.includes('Network')) {
throw new Error('Connection failed. Please check your internet and try again.');
} else {
// Log unexpected errors for debugging
console.error('Unexpected error:', error);
throw new Error('An unexpected error occurred. Please try again or contact support.');
}
}
}
validateTokenData(data) {
const errors = [];
if (!data.name?.trim()) errors.push('Token name is required');
if (!data.symbol?.trim()) errors.push('Token symbol is required');
if (data.symbol && data.symbol.length > 10) {
errors.push('Token symbol must be 10 characters or less');
}
if (data.caEnding && data.caEnding.length > 3) {
errors.push('CA ending must be 3 characters or less');
}
if (data.caEnding && /[0OIl]/.test(data.caEnding)) {
errors.push('CA ending contains invalid characters (0, O, I, l not allowed)');
}
if (data.website && !this.isValidURL(data.website)) {
errors.push('Please enter a valid website URL');
}
if (errors.length > 0) {
throw new Error(errors.join('. '));
}
}
isValidURL(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}
}