Comparing glam with tenderly

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/glam/SKILL.md

GLAM Protocol Skill

GLAM provides programmable investment infrastructure on Solana: vaults with access control, DeFi integrations, and tokenization.

Quick Start

# Install CLI
npm install -g @glamsystems/glam-cli

# Configure (~/.config/glam/config.json)
cat > ~/.config/glam/config.json << 'EOF'
{
  "keypair_path": "~/.config/solana/id.json",
  "json_rpc_url": "https://api.mainnet-beta.solana.com"
}
EOF

# Create vault, set active, enable integrations, verify
glam-cli vault create ./vault-template.json
glam-cli vault set <VAULT_STATE_PUBKEY>
glam-cli integration enable JupiterSwap KaminoLend
glam-cli vault view

Critical: Integration Enablement

You MUST enable integrations BEFORE using them. This is the most common error.

Available: JupiterSwap, DriftProtocol, KaminoLend, KaminoVaults, KaminoFarms, DriftVaults, SplToken, CCTP, GlamMint, Marinade (staging), StakePool (staging), SanctumSingle (staging), SanctumMulti (staging), StakeProgram (staging).

Staging integrations require --bypass-warning.


Workflows

Tokenized Vault Setup

glam-cli vault create ./tokenized-vault-template.json
glam-cli vault set <VAULT_STATE_PUBKEY>
glam-cli integration enable JupiterSwap DriftProtocol KaminoLend
glam-cli manage price                          # Set initial NAV price
glam-cli jupiter set-max-slippage 100          # Configure swap policy

# Optional: delegate trading permissions (protocol-scoped)
glam-cli delegate grant <TRADER_PUBKEY> SwapAny --protocol JupiterSwap
glam-cli delegate grant <TRADER_PUBKEY> Deposit Withdraw CreateModifyOrders CancelOrders --protocol DriftProtocol

# Optional: set timelock (24 hours)
glam-cli timelock set 86400

Drift Trading

glam-cli integration enable DriftProtocol
glam-cli drift-protocol init-user              # Required once
glam-cli drift-protocol deposit 0 1000         # Deposit USDC collateral
glam-cli drift-protocol perp long 0 1 0        # Open position

Kamino Lending

glam-cli integration enable KaminoLend
glam-cli kamino-lend init                      # Required once
glam-cli kamino-lend deposit \
  7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF \
  EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
  1000

Decision Tree

GoalIntegrationCommand
Swap tokensJupiterSwapjupiter swap
Lend for yieldKaminoLendkamino-lend deposit
Stake SOL (liquid)Marinade (staging)marinade --bypass-warning stake
Stake SOL (LST)StakePool / SanctumSingle / SanctumMulti (staging)lst --bypass-warning stake <pool> <amount>
Stake SOL (native)StakeProgram (staging)stake --bypass-warning list / deactivate / withdraw
Kamino vaultsKaminoVaultskamino-vaults deposit
Drift vaultsDriftVaultsdrift-vaults deposit
Trade perpsDriftProtocoldrift-protocol init-userdepositperp
Trade spotDriftProtocoldrift-protocol init-userdepositspot
Tokenized vaultvault createmanage price → investors invest subscribe
Manage share tokensSDK only: client.mint.* (freeze, issue, burn, forceTransfer)
Bridge USDCCCTPcctp bridge-usdc <amount> <domain> <dest> (0=ETH, 1=AVAX, 2=OP, 3=ARB, 6=BASE, 7=POLYGON)
Timelocktimelock set <seconds>

Common Errors

ErrorSolution
"Signer is not authorized"Check vault view for owner; grant delegate if needed
"Integration not enabled"integration enable <NAME>
"Asset not in allowlist"vault allowlist-asset <MINT>
"User not initialized"drift-protocol init-user or kamino-lend init
"No route found"Try smaller amount; check token liquidity
"Slippage exceeded"Increase --slippage-bps or reduce amount
"Insufficient collateral"drift-protocol deposit more
"Account is frozen"SDK: client.mint.setTokenAccountsStates()
"Missing jupiter_api_key"Add jupiter_api_key to config.json

See troubleshooting for detailed solutions.


Common Mints

TokenAddress
SOLSo11111111111111111111111111111111111111112
USDCEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
USDTEs9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
mSOLmSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So
jitoSOLJ1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn

SDK Quick Start

import {
  GlamClient,
  WSOL,
  USDC,
  getProgramAndBitflagByProtocolName,
} from "@glamsystems/glam-sdk";
import { BN } from "@coral-xyz/anchor";

const client = new GlamClient({ wallet });

// Create vault
const { vaultPda } = await client.vault.create({
  name: "My Vault",
  assets: [WSOL, USDC],
});

// Enable Jupiter integration
const perms = getProgramAndBitflagByProtocolName();
const [program, bitflag] = perms["JupiterSwap"];
await client.access.enableProtocols(vaultPda, program, parseInt(bitflag, 2));

// Swap
await client.jupiterSwap.swap(vaultPda, {
  inputMint: USDC,
  outputMint: WSOL,
  amount: new BN(100_000_000),
  slippageBps: 50,
});

Reference

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/tenderly/SKILL.md

Tenderly

Tenderly is a blockchain DevOps platform providing transaction simulation, debugging, monitoring, and testing infrastructure. It supports Ethereum, Polygon, Arbitrum, Optimism, Base, BSC, Avalanche, Fantom, and 60+ other EVM networks through a unified REST API.

What You Probably Got Wrong

LLMs frequently generate outdated Tenderly API calls, confuse v1 and v2 endpoints, and misunderstand how Virtual TestNets differ from simple forks. These corrections are non-negotiable.

  • The API is v2, not v1. All endpoints use https://api.tenderly.co/api/v2/. If you see /api/v1/ in your code, you are using the deprecated API. The v1 simulation endpoint had a different request/response schema. Stop and switch to v2.
  • "Forks" are now "Virtual TestNets". Tenderly rebranded forks to Virtual TestNets. The API paths still use /vnets/ or /testnet/ in some contexts, but the product name is Virtual TestNet. Do not call them "forks" in user-facing code — the API field names may still say fork internally.
  • Authentication uses X-Access-Key, not Authorization: Bearer. The primary auth mechanism is a header X-Access-Key: <your-access-key>. Bearer tokens exist for OAuth flows but are not the standard integration path. If you set Authorization: Bearer <access-key>, the request will 401.
  • Project paths require both account slug AND project slug. Every project-scoped endpoint is /api/v2/project/{accountSlug}/{projectSlug}/.... If you omit either slug, you get a 404. The account slug is your username or org name, NOT your account ID.
  • Simulation from field is required. The v2 simulation endpoint requires a from address. Omitting it does not default to zero address — it returns a 400 error. Always specify who is sending the transaction.
  • State overrides use a map keyed by address, not an array. The state_objects field in simulation requests is { [address]: { storage: { [slot]: value } } }, not an array of override objects.
  • Virtual TestNet RPC URLs expire. When you create a Virtual TestNet, the returned RPC URL is ephemeral. If the VNet is deleted or expires (based on your plan), the URL stops working. Do not hardcode VNet RPC URLs.
  • Web3 Actions are NOT Lambda functions with arbitrary runtimes. They run in a constrained Node.js environment with a specific set of available packages. You cannot install arbitrary npm packages. The runtime provides ethers, axios, and the Tenderly SDK. Check the docs for the current package list.
  • Gas Profiler data comes from simulation, not from live transactions. You must simulate a transaction first, then access the gas breakdown from the simulation result. There is no separate "gas profiler" endpoint.
  • save and save_if_fails are separate flags. Setting save: true saves every simulation to your dashboard. Setting save_if_fails: true only saves failed simulations. They are independent booleans, not mutually exclusive.
  • Simulation network_id is a string, not a number. Pass "1" for Ethereum mainnet, not 1. The API will reject numeric network IDs.
  • block_number in simulations is optional. If omitted, it uses the latest block. If provided, it must be a number (not a string), and it must be a block that has been indexed by Tenderly.

Quick Start

Authentication

Every Tenderly API request requires your access key. Generate one from the Tenderly Dashboard under Settings > Authorization > API Access Tokens.

const TENDERLY_ACCESS_KEY = process.env.TENDERLY_ACCESS_KEY;
const TENDERLY_ACCOUNT_SLUG = process.env.TENDERLY_ACCOUNT_SLUG;
const TENDERLY_PROJECT_SLUG = process.env.TENDERLY_PROJECT_SLUG;

if (!TENDERLY_ACCESS_KEY) throw new Error("TENDERLY_ACCESS_KEY is required");
if (!TENDERLY_ACCOUNT_SLUG) throw new Error("TENDERLY_ACCOUNT_SLUG is required");
if (!TENDERLY_PROJECT_SLUG) throw new Error("TENDERLY_PROJECT_SLUG is required");

const BASE_URL = `https://api.tenderly.co/api/v2/project/${TENDERLY_ACCOUNT_SLUG}/${TENDERLY_PROJECT_SLUG}`;

const headers = {
  "X-Access-Key": TENDERLY_ACCESS_KEY,
  "Content-Type": "application/json",
};

Installation

npm install axios

No dedicated Tenderly SDK package is required for REST API usage. All interaction is through HTTP requests. The @tenderly/actions package is only needed for Web3 Actions development.

Transaction Simulation API

Simulate any transaction without sending it onchain. Returns full execution trace, state changes, gas usage, event logs, and revert reasons.

Simulate a Simple ETH Transfer

interface SimulationRequest {
  network_id: string;
  from: string;
  to: string;
  input: string;
  value: string;
  gas: number;
  gas_price: string;
  save: boolean;
  save_if_fails: boolean;
  simulation_type: "quick" | "full" | "abi";
  state_objects?: Record<string, {
    balance?: string;
    storage?: Record<string, string>;
  }>;
  block_number?: number;
}

async function simulateTransaction(
  params: SimulationRequest
): Promise<{ simulation: { id: string; status: boolean; gas_used: number; block_number: number }; transaction: { transaction_info: { call_trace: { calls: Array<{ from: string; to: string; input: string; output: string; gas_used: number; type: string }> }; state_diff: Array<{ address: string; original: Record<string, string>; dirty: Record<string, string> }>; logs: Array<{ address: string; topics: string[]; data: string }> } } }> {
  const response = await fetch(`${BASE_URL}/simulate`, {
    method: "POST",
    headers,
    body: JSON.stringify(params),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Simulation failed (${response.status}): ${error}`);
  }

  return response.json() as Promise<SimulationResponse>;
}

Simulate an ERC-20 Transfer

import { encodeFunctionData, parseUnits } from "viem";

const erc20TransferAbi = [
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
] as const;

const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const SENDER = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const RECIPIENT = "0x1234567890abcdef1234567890abcdef12345678";

const calldata = encodeFunctionData({
  abi: erc20TransferAbi,
  functionName: "transfer",
  args: [RECIPIENT, parseUnits("1000", 6)],
});

const result = await simulateTransaction({
  network_id: "1",
  from: SENDER,
  to: USDC,
  input: calldata,
  value: "0",
  gas: 100_000,
  gas_price: "0",
  save: true,
  save_if_fails: true,
  simulation_type: "full",
});

console.log(`Success: ${result.simulation.status}`);
console.log(`Gas used: ${result.simulation.gas_used}`);

Simulate with State Overrides

Override account balances or storage slots before simulation. Useful for testing as any address without needing its private key.

const resultWithOverrides = await simulateTransaction({
  network_id: "1",
  from: SENDER,
  to: USDC,
  input: calldata,
  value: "0",
  gas: 200_000,
  gas_price: "0",
  save: false,
  save_if_fails: true,
  simulation_type: "full",
  // Override sender's ETH balance to 100 ETH
  state_objects: {
    [SENDER]: {
      balance: "0x56BC75E2D63100000", // 100 ETH in hex
    },
  },
});

Simulation Types

TypeDescriptionUse Case
quickBasic pass/fail, gas used, no traceCI/CD validation, quick checks
fullFull call trace, state diffs, logsDebugging, security analysis
abiDecoded inputs/outputs using verified ABIHuman-readable output

Batch Simulation (Simulate Bundle)

Simulate multiple transactions sequentially, where each transaction sees the state changes from previous ones.

interface BundleSimulationRequest {
  simulations: SimulationRequest[];
}

async function simulateBundle(
  simulations: SimulationRequest[]
): Promise<SimulationResponse[]> {
  const response = await fetch(`${BASE_URL}/simulate-bundle`, {
    method: "POST",
    headers,
    body: JSON.stringify({ simulations }),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Bundle simulation failed (${response.status}): ${error}`);
  }

  const data = await response.json();
  return data.simulation_results as SimulationResponse[];
}

Virtual TestNets (Forks)

Virtual TestNets create a copy of any network's state at a specific block. You get a private RPC endpoint that behaves like a real network but is fully isolated. Transactions on a Virtual TestNet do not affect mainnet.

Create a Virtual TestNet

async function createVirtualTestNet(params: {
  slug: string;
  display_name: string;
  fork_config: { network_id: number; block_number?: number };
  virtual_network_config: { chain_config: { chain_id: number } };
  sync_state_config: { enabled: boolean };
  explorer_page_config: { enabled: boolean; verification_visibility: "bytecode" | "src" | "all" };
}): Promise<{ id: string; rpcs: Array<{ name: string; url: string }>; fork_config: { network_id: number; block_number: number } }> {
  const response = await fetch(
    `https://api.tenderly.co/api/v2/project/${TENDERLY_ACCOUNT_SLUG}/${TENDERLY_PROJECT_SLUG}/vnets`,
    { method: "POST", headers, body: JSON.stringify(params) }
  );

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`VNet creation failed (${response.status}): ${error}`);
  }

  return response.json();
}

const vnet = await createVirtualTestNet({
  slug: "mainnet-test",
  display_name: "Mainnet Testing Fork",
  fork_config: {
    network_id: 1,
  },
  virtual_network_config: {
    chain_config: {
      chain_id: 73571, // custom chain ID to avoid collision
    },
  },
  sync_state_config: {
    enabled: false,
  },
  explorer_page_config: {
    enabled: true,
    verification_visibility: "src",
  },
});

// Use the RPC URL with viem or ethers
const rpcUrl = vnet.rpcs[0].url;
console.log(`VNet RPC: ${rpcUrl}`);

Fund an Account on a Virtual TestNet

async function fundAccount(
  rpcUrl: string,
  address: string,
  amountHex: string
): Promise<void> {
  const response = await fetch(rpcUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: "tenderly_setBalance",
      params: [[address], amountHex],
      id: 1,
    }),
  });

  if (!response.ok) {
    throw new Error(`Fund account failed (${response.status})`);
  }
}

// Fund address with 1000 ETH
await fundAccount(
  rpcUrl,
  "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "0x3635C9ADC5DEA00000" // 1000 ETH in hex wei
);

Custom RPC Methods on Virtual TestNets

MethodDescription
tenderly_setBalanceSet ETH balance for one or more addresses
tenderly_addBalanceAdd ETH to existing balance
tenderly_setErc20BalanceSet ERC-20 token balance for an address
tenderly_setStorageAtSet arbitrary storage slot value
evm_snapshotCreate a state snapshot (returns snapshot ID)
evm_revertRevert to a previous snapshot
evm_increaseTimeAdvance block timestamp
evm_increaseBlocksMine a specified number of blocks

Set ERC-20 Balance on Virtual TestNet

async function setErc20Balance(
  rpcUrl: string,
  tokenAddress: string,
  walletAddress: string,
  amountHex: string
): Promise<void> {
  const response = await fetch(rpcUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      method: "tenderly_setErc20Balance",
      params: [tokenAddress, walletAddress, amountHex],
      id: 1,
    }),
  });

  if (!response.ok) {
    throw new Error(`Set ERC-20 balance failed (${response.status})`);
  }
}

Delete a Virtual TestNet

async function deleteVirtualTestNet(vnetId: string): Promise<void> {
  const response = await fetch(
    `https://api.tenderly.co/api/v2/project/${TENDERLY_ACCOUNT_SLUG}/${TENDERLY_PROJECT_SLUG}/vnets/${vnetId}`,
    {
      method: "DELETE",
      headers,
    }
  );

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`VNet deletion failed (${response.status}): ${error}`);
  }
}

Alerts and Webhooks

Monitor onchain events in real time. Alerts trigger when specified conditions are met and can notify via webhook, email, Slack, Telegram, PagerDuty, or Discord.

Alert Types

TypeTrigger
successful_txTransaction executes successfully
failed_txTransaction reverts
function_callSpecific function is called on a contract
event_emittedSpecific event is emitted
state_changeStorage slot value changes
balance_changeETH or token balance changes beyond threshold
block_minedNew block is mined (useful for health checks)
whitelisted_callerTransaction from a specific address
blacklisted_callerTransaction NOT from a whitelisted address

Create an Alert via API

async function createAlert(params: {
  name: string;
  network: string;
  type: string;
  enabled: boolean;
  alert_targets: Array<{
    type: "webhook" | "email" | "slack" | "telegram" | "pagerduty" | "discord";
    webhook?: { url: string; secret?: string };
    email?: { address: string };
  }>;
  alert_parameters: {
    contracts?: string[];
    events?: Array<{ name: string; signature: string }>;
    functions?: Array<{ name: string; signature: string }>;
    threshold?: { amount: string; direction: "above" | "below" | "both" };
  };
}): Promise<{ id: string }> {
  const response = await fetch(`${BASE_URL}/alerts`, {
    method: "POST",
    headers,
    body: JSON.stringify(params),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Alert creation failed (${response.status}): ${error}`);
  }

  return response.json() as Promise<{ id: string }>;
}

Webhook Signature Verification

If you set a secret on the webhook target, Tenderly signs the payload with HMAC-SHA256 in the x-tenderly-signature header. Verify with timingSafeEqual from node:crypto. See the setup-alerts example for a complete webhook receiver implementation.

Web3 Actions (Serverless Functions)

Web3 Actions are serverless functions that execute in response to onchain events, periodic schedules, or webhook triggers. They run in Tenderly's Node.js runtime with access to ethers, axios, and the Tenderly SDK.

Project Setup

npm install -g @tenderly/actions-cli
mkdir my-actions && cd my-actions
tenderly actions init

This creates a tenderly.yaml configuration file and an actions directory.

Configuration (tenderly.yaml)

account_id: ""
actions:
  your-account/your-project:
    runtime: v2
    sources: actions
    specs:
      onTransferDetected:
        description: "Triggers on ERC-20 Transfer events"
        function: actions/transfer:onTransfer
        trigger:
          type: transaction
          transaction:
            status:
              - mined
            filters:
              - network: 1
                eventEmitted:
                  contract:
                    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
                  name: Transfer
      periodicCheck:
        description: "Runs every 5 minutes"
        function: actions/health:periodicCheck
        trigger:
          type: periodic
          periodic:
            cron: "*/5 * * * *"
      webhookHandler:
        description: "Handles external webhook calls"
        function: actions/webhook:handleWebhook
        trigger:
          type: webhook

Action Function Signature

import {
  ActionFn,
  Context,
  Event,
  TransactionEvent,
  PeriodicEvent,
  WebhookEvent,
} from "@tenderly/actions";

// Transaction-triggered action
export const onTransfer: ActionFn = async (
  context: Context,
  event: Event
) => {
  const txEvent = event as TransactionEvent;
  const txHash = txEvent.hash;
  const logs = txEvent.logs;

  // Access project secrets (stored in Tenderly dashboard)
  const apiKey = await context.secrets.get("EXTERNAL_API_KEY");

  // Access project storage (key-value store persisted between runs)
  const lastProcessed = await context.storage.getStr("lastProcessedBlock");
  await context.storage.putStr("lastProcessedBlock", txEvent.blockNumber.toString());

  console.log(`Processing tx: ${txHash}`);
};

// Periodic action
export const periodicCheck: ActionFn = async (
  context: Context,
  event: Event
) => {
  const periodic = event as PeriodicEvent;
  console.log(`Periodic run at: ${periodic.time}`);
};

// Webhook-triggered action
export const handleWebhook: ActionFn = async (
  context: Context,
  event: Event
) => {
  const webhook = event as WebhookEvent;
  const body = webhook.payload;
  console.log(`Webhook received: ${JSON.stringify(body)}`);
};

Deploy Actions

tenderly actions deploy

Action Runtime Constraints

ConstraintLimit
Execution timeout60 seconds
Memory256 MB
Payload size (webhook)1 MB
Storage (per project)10 MB
Available packagesethers, axios, @tenderly/actions (built-in)

Contract Verification

Verify contracts on Tenderly for decoded transaction traces and human-readable function calls.

Verify via API

POST to /project/{account}/{project}/contracts/verify with a body containing:

{
  contracts: [{
    contractToVerify: "0xAddress:1",          // "address:network_id"
    solcConfig: {
      compiler_version: "v0.8.24+commit.e11b9ed9",
      optimizations_used: true,
      optimizations_count: 200,
    },
    sources: {
      "Contract.sol": { name: "Contract.sol", code: "// solidity source..." },
    },
  }]
}

Verify Using Hardhat Plugin

npm install @tenderly/hardhat-tenderly
// hardhat.config.ts
import "@tenderly/hardhat-tenderly";

const config = {
  tenderly: {
    project: process.env.TENDERLY_PROJECT_SLUG,
    username: process.env.TENDERLY_ACCOUNT_SLUG,
  },
};
npx hardhat tenderly:verify --network mainnet ContractName=0x...

Gas Profiler

Gas profiling data is embedded in simulation_type: "full" results. There is no separate gas profiler endpoint. Walk the transaction.transaction_info.call_trace.calls array to get per-call gas breakdowns including from, to, gas_used, and type for each internal call. See the simulate-transaction example for a complete extractGasProfile implementation.

Transaction Debugger

The Transaction Debugger provides step-by-step EVM execution traces for any transaction. Access it via the dashboard or programmatically.

Get Debug Trace for an Existing Transaction

async function getTransactionTrace(
  networkId: string,
  txHash: string
): Promise<{ gas: number; failed: boolean; return_value: string; struct_logs: Array<{ pc: number; op: string; gas: number; gas_cost: number; depth: number; stack: string[]; memory: string[]; storage: Record<string, string> }> }> {
  const response = await fetch(
    `https://api.tenderly.co/api/v2/project/${TENDERLY_ACCOUNT_SLUG}/${TENDERLY_PROJECT_SLUG}/network/${networkId}/transaction/${txHash}/trace`,
    { method: "GET", headers }
  );

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Trace retrieval failed (${response.status}): ${error}`);
  }

  return response.json();
}

Error Handling Pattern

All Tenderly API errors return { error: { message, slug, id } }. Always parse the error body on non-2xx responses to get actionable error slugs. See resources/error-codes.md for the complete error reference and templates/tenderly-client.ts for a production-ready tenderlyFetch<T>() wrapper with proper error extraction.

Rate Limits and Quotas

ResourceFreeProEnterprise
Simulations/month1,50050,000Custom
Virtual TestNets (concurrent)110Custom
Alerts5100Custom
Web3 Actions325Custom
API rate limit10 req/s50 req/sCustom

Rate limit responses return HTTP 429 with a Retry-After header.

CI/CD Integration

Simulate Before Deploy (GitHub Actions)

name: Simulate Deployment
on:
  pull_request:
    paths:
      - "contracts/**"

jobs:
  simulate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npx hardhat compile
      - name: Simulate deployment
        env:
          TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }}
          TENDERLY_ACCOUNT_SLUG: ${{ secrets.TENDERLY_ACCOUNT_SLUG }}
          TENDERLY_PROJECT_SLUG: ${{ secrets.TENDERLY_PROJECT_SLUG }}
        run: npx ts-node scripts/simulate-deploy.ts

Further Reading

AI Skill Finder

Ask me what skills you need

What are you building?

Tell me what you're working on and I'll find the best agent skills for you.