# Complete Agent Authentication Callback Source: https://docs.bags.fm/api-reference/agent-auth-callback POST /agent/v2/auth/callback Submit either a wallet signature callback payload or an MFA callback payload to receive an API key. # Initialize Agent Authentication Source: https://docs.bags.fm/api-reference/agent-auth-init POST /agent/v2/auth/init Generate a signable challenge message and nonce for wallet-signature based agent authentication. # Claim DAMM v2 Vault Source: https://docs.bags.fm/api-reference/claim-damm-v2-vault POST /token-launch/damm-v2/claim-vault Sweeps the caller's partner or deployer aggregate vault for a quote mint to their wallet. Returns a gas-sponsored transaction: the gas sponsor is the fee payer, and the partner/deployer wallet co-signs client-side as the authorizer. # Create DAMM v2 Launch Transaction Source: https://docs.bags.fm/api-reference/create-damm-v2-launch-transaction POST /token-launch/damm-v2/create-transaction Builds the partially-signed transaction bundle that launches a token previously registered via `POST /token-launch/create-token-info` straight into a single-sided DAMM v2 customizable pool (no DBC bonding curve, no migration). The token's launch status must still be `PRE_LAUNCH`. Every transaction in the bundle must be co-signed by `wallet` before submission; see the bundle item `type` for submission order (`LUT_SETUP` and `CREATE_TOKEN` first, then `LAUNCH`, then optionally `LUT_DEACTIVATE`/`LUT_CLOSE`). ## Submission order The response bundle can contain up to five transactions. Submit them in this order: 1. **`LUT_SETUP`** and **`CREATE_TOKEN`** — independent; submit both (in parallel is fine) and wait for both to confirm. `LUT_SETUP` creates the address lookup table `LAUNCH` depends on; a lookup table is only usable starting the slot *after* it is extended, so confirming this before submitting `LAUNCH` is required. `CREATE_TOKEN` is omitted from the bundle on rebuilds where the mint already landed on-chain. 2. **`LAUNCH`** — atomic: initializes the pool, deposits both locked positions, and executes the optional initial buy. Trading is live the moment this lands. If the first submission races the lookup-table warmup, retry once the table has warmed up. 3. **`LUT_DEACTIVATE`** — deactivates the lookup table. Submit only after `LAUNCH` confirms. Carries no server signature — replace `recentBlockhash` with a fresh one right before signing. 4. **`LUT_CLOSE`** (optional) — reclaims the lookup table rent. Only valid for a window starting shortly after `LUT_DEACTIVATE`. Carries no server signature — replace `recentBlockhash` with a fresh one right before signing. Every transaction must be co-signed by `wallet`. If the bundle's blockhash expires before `LAUNCH` lands, request a new bundle rather than resubmitting — nothing partial is left on-chain. # Create Dexscreener Order Source: https://docs.bags.fm/api-reference/create-dexscreener-order POST /solana/dexscreener/create-order Create a Dexscreener token info order. Returns a payment transaction that must be signed and submitted. # Create Fee Share Admin Transfer Transaction Source: https://docs.bags.fm/api-reference/create-fee-share-admin-transfer-tx POST /fee-share/admin/transfer-tx Creates a transaction to transfer fee share admin authority from the current admin to a new admin for a given token. # Create Fee Share Admin Update Config Source: https://docs.bags.fm/api-reference/create-fee-share-admin-update-config POST /fee-share/admin/update-config Creates transactions to update the fee share configuration for a token. Allows the admin to change the fee claimers and their basis point allocations. # Create Fee Share Config creation transaction Source: https://docs.bags.fm/api-reference/create-fee-share-configuration POST /fee-share/config Create a fee sharing config with multiple fee claimers (up to 100). Token Launches require fee sharing configuration for all token launches. All fees must be explicitly allocated using basis points. When there are more than 15 fee claimers, lookup tables are required. # Create Partner Config Source: https://docs.bags.fm/api-reference/create-partner-configuration POST /fee-share/partner-config/creation-tx Get a transaction to create a partner key (partner config) for fee sharing. Only one partner key can be created per wallet. # Create Robinhood Claim Transactions Source: https://docs.bags.fm/api-reference/create-rh-claim-txs POST /evm/rh/create-claim-txs Create unsigned EVM transactions for an owner to claim a Robinhood Chain token's accrued fees as native ETH. The response includes the current claimable and estimated pending amounts used to build the transaction. Sign and submit `transactions` in array order, waiting for each receipt before sending the next. A token normally returns one transaction because it currently has one fee-share contract. Sign and submit `transactions` in array order, waiting for each receipt before sending the next. Claims unwrap WETH to native ETH. # Create Swap Transaction Source: https://docs.bags.fm/api-reference/create-swap-transaction POST /trade/swap Create a swap transaction from a trade quote. The transaction is ready to be signed and sent. # Create Token Info and Metadata Source: https://docs.bags.fm/api-reference/create-token-info POST /token-launch/create-token-info Create token information with image upload and generate a token mint that can be used to launch a token # Create Token Launch Transaction Source: https://docs.bags.fm/api-reference/create-token-launch-transaction POST /token-launch/create-launch-transaction Create a token launch transaction (already signed with token mint) # Get Current User Info Source: https://docs.bags.fm/api-reference/get-auth-me GET /auth/me Returns information about the user that owns the provided public API key. # Get Bags Pool by Token Mint Source: https://docs.bags.fm/api-reference/get-bags-pool-by-token-mint GET /solana/bags/pools/token-mint Retrieve a single Bags pool by its token mint address, with its associated Meteora DBC and DAMM v2 pool keys # Get Bags Pools Source: https://docs.bags.fm/api-reference/get-bags-pools GET /solana/bags/pools Retrieve a list of all Bags pools with their associated Meteora DBC and DAMM v2 pool keys # Get Claim Transactions Source: https://docs.bags.fm/api-reference/get-claim-transactions POST /token-launch/claim-txs/v2 Generate transactions to claim fees from virtual pools and/or DAMM v2 positions or custom fee vaults. Supports both v1 and v2 fee share programs. # Get Claim Transactions (v3) Source: https://docs.bags.fm/api-reference/get-claim-transactions-v3 POST /token-launch/claim-txs/v3 Generate transactions to claim fees for a token. This simplified v3 endpoint automatically handles all fee claiming logic based on the token state. The simplified v3 claim endpoint automatically handles all fee claiming logic. Just provide the fee claimer wallet and token mint — the API determines the position type and builds the appropriate transactions. For a complete walkthrough, see the [Claim Fees guide](/how-to-guides/claim-fees). # Get Claimable Positions Source: https://docs.bags.fm/api-reference/get-claimable-positions GET /token-launch/claimable-positions Retrieve all claimable fee positions for a wallet. Returns positions with fee information from virtual pools and DAMM v2. Returns all claimable fee positions for a wallet. Use this to inspect position details and claimable amounts before claiming. This endpoint is optional for claiming fees. The [v3 claim endpoint](/api-reference/get-claim-transactions-v3) handles position lookup automatically — you only need this if you want to inspect positions beforehand. # Get DAMM v2 Direct Launches Source: https://docs.bags.fm/api-reference/get-damm-v2-launches GET /token-launch/damm-v2/launches Newest-first, paginated list of confirmed DAMM v2 direct launches, optionally filtered by quote mint. Only launches whose pool has been confirmed on-chain are returned. # Get DAMM v2 Supported Quote Tokens Source: https://docs.bags.fm/api-reference/get-damm-v2-supported-quote-tokens GET /token-launch/damm-v2/supported-quote-tokens Every quote mint currently usable for DAMM v2 direct launches: all mints holding a cp-amm TokenBadge, enriched with decimals, owning token program, Token 2022 metadata, and an image. # Get DAMM v2 Vault Claimables Source: https://docs.bags.fm/api-reference/get-damm-v2-vault-claimables GET /token-launch/damm-v2/vault-claimables Every non-empty partner/deployer aggregate vault balance for a wallet. Partners and deployers do not own a DAMM v2 position directly; their revenue share accrues into a per-wallet aggregate vault (one per quote mint) that is swept with this and the claim-vault endpoint. # Check Dexscreener Order Availability Source: https://docs.bags.fm/api-reference/get-dexscreener-order-availability GET /solana/dexscreener/order-availability Check whether a Dexscreener token info order is available for a given token. # Get EVM Token Creators Source: https://docs.bags.fm/api-reference/get-evm-token-creator GET /evm/token-creator Resolve the creator and fee-share claimers of a Bags EVM token, enriched with Bags social profile data (username, pfp, connected social provider). On-chain data (creator, claimers, and their basis-point shares) is read from the Bags EVM factory registry. Each wallet is resolved to a Bags user, and wallets with no known Bags user are returned with blank social fields. The creator is always included; if the creator is not also a claimer, they are appended with `royaltyBps: 0` and `isCreator: true`. The `tokenAddress` is trimmed and normalized to its EIP-55 checksum form before use. A token with a single creator/claimer still returns a one-element array. Provide a checksummed or lowercase `0x` address; the backend trims it and normalizes it to EIP-55 checksum form. The response is always an array — a token with a single creator/claimer still returns a one-element array. Wallets without a linked Bags user are returned with `provider: "unknown"` and empty `username`/`pfp`. # Get Fee Share Admin List Source: https://docs.bags.fm/api-reference/get-fee-share-admin-list GET /fee-share/admin/list Returns a list of token mints where the given wallet is the fee share admin. # Get Fee Share Wallet V2 Source: https://docs.bags.fm/api-reference/get-fee-share-wallet GET /token-launch/fee-share/wallet/v2 Get the wallet address associated with a social provider and username for fee sharing. The optional `chain` parameter selects which chain to resolve the wallet on: `SOL` (default) returns the user's Solana embedded wallet, while `EVM` resolves (and creates if needed) the user's Ethereum embedded wallet from Privy. The resolved `chain` is always echoed back in the response. ## Choosing a chain The optional `chain` parameter selects which embedded wallet to resolve: * **`SOL`** (default when omitted) — returns the user's Solana embedded wallet as a base58 address. * **`EVM`** — resolves the user's Ethereum embedded wallet from Privy, creating one if the user has none, and returns a `0x…` address. The resolved `chain` is always echoed back in the response, defaulting to `SOL` when the parameter was omitted. The `solana` provider is a raw wallet-address passthrough (the `username` is the Solana public key) and only supports `SOL`. Combining `provider=solana` with `chain=EVM` returns a `400` error. # Get Fee Share Wallet V2 (Bulk) Source: https://docs.bags.fm/api-reference/get-fee-share-wallet-bulk POST /token-launch/fee-share/wallet/v2/bulk Bulk lookup of wallet addresses associated with social providers and usernames for fee sharing. Each item accepts an optional `chain` (`SOL` default, or `EVM`); the resolved `chain` is echoed back on every result. Deduplication is performed on `username + provider + chain`, so the same handle may appear once per chain. Items are never individually failed: when a wallet or platform data can't be resolved, that item returns `platformData: null` and `wallet: null` while still echoing `chain`. ## Choosing a chain per item Each item accepts an optional `chain` field: * **`SOL`** (default when omitted) — returns the user's Solana embedded wallet as a base58 address. * **`EVM`** — resolves the user's Ethereum embedded wallet from Privy, creating one if the user has none, and returns a `0x…` address. The resolved `chain` is always echoed back on every result item. ## Deduplication Deduplication is performed on the `username + provider + chain` triple. The same `username + provider` may appear twice in one request as long as the `chain` differs (once for `SOL`, once for `EVM`). Two items with an identical `username + provider + chain` triple are rejected with a `400` error. Bulk lookups never fail an individual item: when a wallet or platform data can't be resolved, that item comes back with `platformData: null` and `wallet: null`, but `chain` is always echoed back. The `solana` provider only supports `SOL`; combining it with `chain=EVM` returns a `400` error. # Get Global Claim Feed v2 Source: https://docs.bags.fm/api-reference/get-global-claim-feed-v2 GET /feed/global-claim/v2 Retrieve the feed of fee-claim events across all tokens, newest first. Each event carries the mint the amount was claimed in, that mint's decimals, and a read-time USD value. v2 covers all three claim sources — bonding-curve claims, fee-share v2 claims, and DAMM v2 direct custody claims — so events denominated in a pool's quote mint rather than SOL are included. Page backwards by passing the last event's `timestamp` as `before`; `hasMore` reports whether older events exist. Forced claims are excluded from this feed. # Create Partner Claim Transactions Source: https://docs.bags.fm/api-reference/get-partner-claim-transactions POST /fee-share/partner-config/claim-tx Generate transactions to claim accumulated partner fees # Get Partner Stats Source: https://docs.bags.fm/api-reference/get-partner-stats GET /fee-share/partner-config/stats Retrieve partner statistics including claimed and unclaimed fees for a given partner # Get Pool Config Keys by Fee Claimer Vaults Source: https://docs.bags.fm/api-reference/get-pool-config-keys POST /token-launch/state/pool-config Given a list of fee claimer vault public keys, returns the first Meteora DBC pool config key for each if present. WARNING: This function will assume there is only one config key for a fee claimer vault. If this is used for non bags-fee-share fee claimer vault, it will return the first config key found. # Get Robinhood Balances Source: https://docs.bags.fm/api-reference/get-rh-balances GET /evm/rh/balances Get an owner's ETH and WETH balances on Robinhood Chain — plus an optional token balance — in one round-trip. # Get Robinhood Claimable Positions Source: https://docs.bags.fm/api-reference/get-rh-claimable-positions GET /evm/rh/claimable-positions Get every Robinhood Chain fee position the owner can act on across V1 and V2 tokens. Each position includes already-notified claimable WETH, the owner's estimated share of un-swept hook fees, lifetime claimed amounts, and claimer or partner eligibility. `pendingWei` remains an estimate until the claim executes. When `truncated` is `true`, token discovery reached a safety bound and `positions` may be incomplete. `pendingWei` is an estimate until the claim executes and sweeps accrued hook fees. When `truncated` is `true`, `positions` may be incomplete. # Get Robinhood Creation Fee Source: https://docs.bags.fm/api-reference/get-rh-creation-fee GET /evm/rh/creation-fee Get the current token creation fee on the Bags V2 factory on Robinhood Chain. # Get Robinhood Creator Earnings Source: https://docs.bags.fm/api-reference/get-rh-creator-earnings GET /evm/rh/creator-earnings Get a single user's earnings view for a Bags V2 token on Robinhood Chain: claimer/partner status, claimable, claimed, and estimated pending amounts. Returns `404` for non-V2 tokens. # Get Robinhood Creator Fees Source: https://docs.bags.fm/api-reference/get-rh-creator-fees GET /evm/rh/creator-fees Get lifetime and pending fees for a Bags V2 token on Robinhood Chain. Lifetime figures are summed from subgraph fee notifications; the pending leg is the hook's un-swept accrual. Lifetime earnings for display = notified sums + pending. Returns `404` for non-V2 tokens. A token's total lifetime earnings for display = `lifetimeCreatorFeesWei` + `lifetimePartnerFeesWei` + `pendingHookFeesWei` (the un-swept accrual still sitting on the hook). # Get Robinhood Creator Roster Source: https://docs.bags.fm/api-reference/get-rh-creator-roster GET /evm/rh/creator-roster Get the full fee-share roster for a Bags V2 token on Robinhood Chain — every recipient with claimed, claimable, and estimated pending amounts. Combines the fee-share claimer list, live claimable per recipient, the partner and rate from the hook snapshot, per-recipient claim sums from the subgraph, and the estimated pending split. Returns `404` for non-V2 tokens. # Get Index Token History Source: https://docs.bags.fm/api-reference/get-rh-index-token-history GET /evm/rh/index-token/history Get completed distribution cycles for an index token on Robinhood Chain, newest first. Each cycle covers the fee claim, the basket token buys it funded, the holder distribution (with snapshot stats), and the top 10 recipients. For the next page pass `nextCursor` back as `cursor`. `nextCursor` is `null` when the history is exhausted. # Get Index Token Statuses Source: https://docs.bags.fm/api-reference/get-rh-index-token-status POST /evm/rh/index-token/status Batch-check whether tokens on Robinhood Chain are index tokens. Returns one entry per unique input address with the index-token flag and, when applicable, the basket token addresses the index token buys and distributes. # Get Robinhood Pool Price Source: https://docs.bags.fm/api-reference/get-rh-pool-price GET /evm/rh/pool-price Get the live Uniswap V4 pool spot price for a migrated Bags V2 token on Robinhood Chain (read from the pool's slot0). `priceEthPerToken` is `null` for non-migrated tokens or when the pool read is unavailable — render as "unavailable", never `0`. Returns `404` for non-V2 tokens. `priceEthPerToken` is `null` for non-migrated tokens or when the pool read is unavailable. Render `null` as "unavailable" — never `0`. # Get Robinhood Portfolio Source: https://docs.bags.fm/api-reference/get-rh-portfolio GET /evm/rh/portfolio Get an owner's Bags V2 portfolio on Robinhood Chain. - `holdings`: tokens the owner has traded and still holds, with live list enrichment and balance. - `earnings`: every token the owner created, is partner on, or has claimed from — including launches with no earnings yet (`lifetimeWei` `"0"`). Each row carries the `feeShare` contract to claim against, `claimableWei`, and `lifetimeWei` (the owner's claimed + currently-claimable total, so claimed-out launches still list). `truncated` is `true` only when created/partner-token discovery hit the ~10k-token safety bound before exhausting the owner's tokens, in which case `earnings` is a subset — surface "some earnings may be missing" rather than treating the list as complete. `earnings` lists every token the owner created, is partner on, or has claimed from — including launches with no earnings yet (`lifetimeWei` `"0"`) and claimed-out launches (`lifetimeWei` = claimed + currently claimable). When `truncated` is `true`, earnings discovery hit its safety bound and `earnings` is a subset — surface "some earnings may be missing" rather than treating the list as complete. # Get Robinhood Swap Quote Source: https://docs.bags.fm/api-reference/get-rh-quote GET /evm/rh/quote Get a display-only swap quote for a Bags V2 token on Robinhood Chain. Pre-migration quotes come from the bonding curve; post-migration quotes come from the Uniswap V4 Quoter. **Exact-in only** — exact-out is not supported. Quotes are display-only and uncached: every request quotes against the current block, and clients must re-quote client-side at sign time. Returns `404` for non-V2 tokens. Quotes are **display-only** — always re-quote client-side at sign time. Every request quotes against the current block (responses are never cached). Quotes are **exact-in only**: `amountWei` is always the input amount (ETH wei for buys, token base units for sells). Exact-out is not supported. # Get Robinhood Token Source: https://docs.bags.fm/api-reference/get-rh-token GET /evm/rh/token Get a single Bags V2 token on Robinhood Chain: indexed data plus live on-chain state. **Returns `404` when the address is not a V2 token** — use this as the V1-vs-V2 routing signal. Freshly launched tokens can 404 for a few seconds while the subgraph indexes, so launch flows should route explicitly rather than relying on this endpoint immediately after launch. A `404` means the address is not a Bags V2 token — use this as the V1-vs-V2 routing signal. Freshly launched tokens can `404` for a few seconds while the subgraph indexes, so route launch flows explicitly instead of relying on this endpoint immediately after launch. # Get Robinhood Token Creations Source: https://docs.bags.fm/api-reference/get-rh-token-creations GET /evm/rh/token-creations Get recently launched Bags V2 tokens on Robinhood Chain, newest first. # Get Robinhood Token State Source: https://docs.bags.fm/api-reference/get-rh-token-state GET /evm/rh/token-state Get live on-chain state for a Bags V2 token on Robinhood Chain (via the V2 lens), with no subgraph dependency. Returns `404` for non-V2 tokens. # Get Robinhood Token List Source: https://docs.bags.fm/api-reference/get-rh-tokens GET /evm/rh/tokens Get a paged list of Bags V2 tokens on Robinhood Chain with live enrichment (spot price and bonding progress). Each item carries `priceEthPerToken` (nullable) and `bondingProgressPct`, plus resolved `metadata` (image, description). `total` is bounded by an internal count-scan cap (10k): when `totalTruncated` is `true` the real total is higher, so treat `total` as a lower bound and keep paging until `items` comes back short instead of using it to conclude pagination is exhausted. `total` is bounded by an internal count-scan cap (10k). When `totalTruncated` is `true`, the real total is higher — treat `total` as a lower bound and keep paging until `items` comes back short, rather than using `total` to decide pagination is exhausted. # Get Robinhood Top Volume Source: https://docs.bags.fm/api-reference/get-rh-top-volume GET /evm/rh/top-volume Get the top 100 Bags V2 tokens on Robinhood Chain by lifetime volume, ranked on indexed gross trading fees. Each item carries `volumeEthWei`, the inferred lifetime ETH volume, plus the same live enrichment (`priceEthPerToken`, `bondingProgressPct`) as the token list endpoint. # Get Robinhood Trade Stats Source: https://docs.bags.fm/api-reference/get-rh-trade-stats GET /evm/rh/trade-stats Get a server-side trade aggregate for a Bags V2 token on Robinhood Chain over a rolling window (volume, transaction counts, last price). The aggregate streams the full window from the subgraph; `truncated` is `true` only in the pathological case where the window exceeds the ~500k-trade safety bound, in which case `volumeEthWei` and counts are a lower bound. # Get Robinhood Trade History Source: https://docs.bags.fm/api-reference/get-rh-trades GET /evm/rh/trades Get trade history for a Bags V2 token on Robinhood Chain, newest first, using a compound keyset cursor. Every page respects `limit` (never over-returns), and ties within a busy second are never skipped. For the next page pass **both** `nextBeforeTs` back as `beforeTs` and `nextBeforeId` back as `beforeId`. Both are `null` when the history is exhausted. Pagination uses a compound keyset cursor. For the next (older) page, pass **both** `nextBeforeTs` back as `beforeTs` and `nextBeforeId` back as `beforeId`. Both cursors are `null` when the history is exhausted. ```typescript theme={null} const firstPage = await getTrades({ tokenAddress, limit: 100 }); const nextPage = await getTrades({ tokenAddress, limit: 100, beforeTs: firstPage.nextBeforeTs, beforeId: firstPage.nextBeforeId, }); ``` # Get Token Claim Events Source: https://docs.bags.fm/api-reference/get-token-claim-events GET /fee-share/token/claim-events Retrieve claim events for a specific token. Supports two query modes: **Offset Mode** (default): Use `mode=offset` (or omit mode) with `limit` and `offset` for traditional pagination. **Time Mode**: Use `mode=time` with `from` and `to` unix timestamps to retrieve events within a specific time range. Retrieve claim events for a specific token. This endpoint supports two query modes to give you flexibility in how you fetch claim data. ## Query Modes ### Offset Mode (Default) Use offset-based pagination to retrieve claim events in batches. This is the default mode and is backward compatible with previous API versions. ```bash theme={null} curl --request GET \ --url 'https://public-api-v2.bags.fm/api/v1/fee-share/token/claim-events?tokenMint=YOUR_TOKEN_MINT&mode=offset&limit=50&offset=0' \ --header 'x-api-key: YOUR_API_KEY' ``` Set to `offset` or omit entirely for pagination mode. Maximum number of events to return (1-100). Number of events to skip for pagination. ### Time Mode Use time-based filtering to retrieve all claim events within a specific time range. Useful for analytics dashboards, scheduled reports, or syncing historical data. ```bash theme={null} curl --request GET \ --url 'https://public-api-v2.bags.fm/api/v1/fee-share/token/claim-events?tokenMint=YOUR_TOKEN_MINT&mode=time&from=1704067200&to=1706745600' \ --header 'x-api-key: YOUR_API_KEY' ``` Must be set to `time` for time-based filtering. Start unix timestamp (inclusive). Must be greater than or equal to 0. End unix timestamp (inclusive). Must be greater than or equal to `from`. When using time mode, the `from` timestamp must be less than or equal to `to`. The API will return an error if this constraint is violated. ## Use Cases * **Offset Mode**: Best for building paginated UIs, real-time feeds, or when you need the most recent events. * **Time Mode**: Best for analytics, generating reports for specific periods, auditing, or syncing claim history. For a complete implementation example, see the [Get Token Claim Events](/how-to-guides/get-token-claim-events) how-to guide. # Get Token Claim Stats Source: https://docs.bags.fm/api-reference/get-token-claim-stats GET /token-launch/claim-stats Retrieve claim statistics for all fee claimers of a specific token, including total claimed amounts per user. # Get Token Claim Stats v4 Source: https://docs.bags.fm/api-reference/get-token-claim-stats-v4 GET /token-launch/claim-stats/v4 Retrieve claim totals per wallet, broken down by the mint each amount was claimed in and converted to USD at read time. Unlike `/token-launch/claim-stats`, which reports a single lamport total, v4 covers all three claim sources — bonding-curve claims, fee-share v2 claims, and DAMM v2 direct custody claims — so claims denominated in a pool's quote mint rather than SOL are included and never summed into a SOL-denominated field. Query by either `tokenMint` (all wallets that claimed on one token) or `wallet` (all tokens one wallet claimed on), but not both. # Get Token Launch Source: https://docs.bags.fm/api-reference/get-token-launch GET /token-launch Retrieve a single token launch record by its token mint. # Get Token Launch Bulk Source: https://docs.bags.fm/api-reference/get-token-launch-bulk POST /token-launch/bulk Retrieve token launch records for up to 100 token mints in one request. Results preserve input order, with `null` for a mint that has no launch record. # Get Token Launch Creators Source: https://docs.bags.fm/api-reference/get-token-launch-creators GET /token-launch/creator/v3 Retrieve the creators/deployers of a specific token launch. Use the 'provider' field to show a platform logo in your UI, and prefer 'providerUsername' for display when present since 'username' is a Bags internal username and optional. # Get Token Launch Feed Source: https://docs.bags.fm/api-reference/get-token-launch-feed GET /token-launch/feed Retrieve the token launch feed containing recent and active token launches with their current status. # Get Token Lifetime Fees Source: https://docs.bags.fm/api-reference/get-token-lifetime-fees GET /token-launch/lifetime-fees Retrieve the total lifetime fees collected for a specific token # Get Trade Quote Source: https://docs.bags.fm/api-reference/get-trade-quote GET /trade/quote Get a quote for swapping tokens. Returns expected output amount, price impact, slippage, and route plan. # Initialize Index Token Source: https://docs.bags.fm/api-reference/init-rh-index-token POST /evm/rh/index-token/init Initialize a Bags V2 token on Robinhood Chain as an index token with a basket of tokens to buy and distribute. The token's on-chain creator wallet must belong to the API key's user, and the token must have the required index-token claimer configured. # API Reference Source: https://docs.bags.fm/api-reference/introduction Complete documentation for all Bags API endpoints ## Welcome The Bags API provides comprehensive functionality for managing API keys and launching tokens on the Solana blockchain. This reference documents all available endpoints with detailed examples. ## Base URL All API endpoints use the following base URL: ``` https://public-api-v2.bags.fm/api/v1/ ``` ## Authentication The Bags API uses API key authentication. Include your API key in the `x-api-key` header: ```bash theme={null} curl -H "x-api-key: YOUR_API_KEY" \ https://public-api-v2.bags.fm/api/v1/endpoint ``` **Required for all endpoints except public analytics endpoints.** Get your API key from the [Bags Developer Dashboard](https://dev.bags.fm). ## Public Key Format Throughout this documentation, when we refer to "public key" we always mean **Base58 encoded public keys**. This is the standard format used by Solana for representing wallet addresses, token mints, and other on-chain accounts. ## Response Format All API responses follow a consistent format: **Success Response:** ```json theme={null} { "success": true, "response": { // Response data here } } ``` **Error Response:** ```json theme={null} { "success": false, "error": "Error message description" } ``` ## Available Endpoints The API provides the following functionality: * **Token Launch**: Create and manage token launches with metadata and initial purchases * **Fee Sharing**: Configure custom fee sharing between wallets * **Analytics**: Retrieve token lifetime fees and creator information * **Fee Claiming**: Generate transactions to claim fees from various sources All endpoints are documented below with request/response schemas, parameters, and examples. # Send Transaction Source: https://docs.bags.fm/api-reference/send-transaction POST /solana/send-transaction Submit a signed Bags Solana transaction to the network for execution. # Submit Dexscreener Payment Source: https://docs.bags.fm/api-reference/submit-dexscreener-payment POST /solana/dexscreener/submit-payment Submit the signed payment transaction for a Dexscreener order. # Changelog Source: https://docs.bags.fm/changelog/changelog Product updates and announcements > For instant change notifications, join and subscribe to our Telegram channel: [Bags Dev Notifications](https://t.me/bags_dev). **Launch tokens quoted in non-SOL assets:** You can now launch a token directly into a Meteora DAMM v2 pool quoted in a badged non-SOL mint (xStocks, Ondo tokenized equities, and other supported quote tokens) — no bonding curve and no migration. Fees accrue and are claimed in the pool's quote token. **Documentation Updates:** * Added [Launch a Token with a Non-SOL Quote Token](/how-to-guides/launch-token-non-sol-quote) how-to guide covering the launch flow, creator fee claiming, and partner/deployer vault sweeping. * Added [Get Claim Transactions (v2)](/api-reference/get-claim-transactions) to the Fee Claiming API reference navigation. **SDK Methods:** * `sdk.tokenLaunch.getDammV2SupportedQuoteTokens()` — list supported quote mints. * `sdk.tokenLaunch.createDammV2LaunchTransaction()` — build the launch bundle. * `sdk.tokenLaunch.getDammV2Launches()` — list confirmed direct launches. * `sdk.tokenLaunch.getDammV2VaultClaimables()` / `sdk.tokenLaunch.claimDammV2Vault()` — sweep partner/deployer vaults. **Get Token Claim Events - Time-Based Filtering:** The `/fee-share/token/claim-events` endpoint now supports two query modes: * **Offset Mode** (default, backward compatible): Use `mode=offset` with `limit` and `offset` for traditional pagination. * **Time Mode** (new): Use `mode=time` with `from` and `to` unix timestamps to retrieve all events within a specific time range. **Example - Time Mode:** ``` GET /fee-share/token/claim-events?tokenMint=...&mode=time&from=1704067200&to=1706745600 ``` **New Parameters:** * `mode`: Query mode (`offset` or `time`). Defaults to `offset` for backward compatibility. * `from`: Start unix timestamp (required for time mode). * `to`: End unix timestamp (required for time mode, must be >= `from`). **Documentation Updates:** * Updated [Get Token Claim Events](/api-reference/get-token-claim-events) API reference with new parameters. * Added [Get Token Claim Events](/how-to-guides/get-token-claim-events) how-to guide with examples for both modes. **Backward Compatibility:** * Existing integrations using offset/limit without specifying `mode` will continue to work unchanged. **Token Launch v2 - Fee Sharing Required:** * Token Launch v2 now requires fee sharing configuration for all token launches. Launches without shared fees are no longer supported. * All fees must be explicitly allocated using basis points. Creators must always set their BPS explicitly, even when receiving 100% of fees. * When sharing fees, both creators and fee claimers must have their BPS set explicitly in the configuration. **New Features** * **Multiple Fee Claimers**: Support for sharing fees with multiple fee claimers (not just 2 users), up to a maximum of 100 fee earners per token launch (including the creator). Each fee claimer can be identified by social provider (twitter, kick, github) and username. * **Partner Configuration**: New partner key system allows platforms and partnerships to receive fees from multiple token launches. See [Create Partner Key](/how-to-guides/create-partner-key) and [Claim Partner Fees](/how-to-guides/claim-partner-fees) guides. * **Lookup Tables (LUTs)**: Automatic LUT creation and management for fee share configs with more than 15 fee claimers. The SDK handles LUT creation, slot waiting, and extension automatically. * **Trade Service**: New trade endpoints for getting quotes and executing token swaps. See [Trade Tokens](/how-to-guides/trade-tokens) guide. **SDK Updates:** * `createBagsFeeShareConfig` now supports `partner` and `partnerConfig` parameters for partner fee sharing. * `createBagsFeeShareConfig` now supports `additionalLookupTables` parameter for configs with >15 fee claimers. * New `getConfigCreationLookupTableTransactions()` helper function for LUT creation. * New `waitForSlotsToPass()` utility function for waiting between LUT creation and extension. * New `getPartnerConfigCreationTransaction()` for creating partner keys. * New `getPartnerConfigClaimTransactions()` for claiming partner fees. * New `getQuote()` and `createSwapTransaction()` for token trading. * Updated `getClaimTransaction()` to return `Transaction[]` instead of `VersionedTransaction[]`. **Documentation Updates:** * Updated [Launch a Token](/how-to-guides/launch-token) guide with v2 flow and multiple fee claimer support. * Removed `launch-token-with-shared-fees` guide (functionality merged into main launch guide). * Added [Create Partner Key](/how-to-guides/create-partner-key) guide with dev dashboard and SDK methods. * Added [Claim Partner Fees](/how-to-guides/claim-partner-fees) guide with dev dashboard and SDK methods. * Added [Trade Tokens](/how-to-guides/trade-tokens) guide for swap functionality. * Updated [Claim Fees](/how-to-guides/claim-fees) guide with new SDK functions and detailed position type explanations. * Updated core principles: Program IDs (added Fee Share V2), Lookup Tables (LUT requirements), Tipping (updated endpoints), File Uploads (corrected endpoint). **Breaking Changes:** * Token launches now require fee sharing configuration. The old flow without fee sharing is no longer supported. * Fee claimers must be identified using supported social providers: `twitter`, `kick`, and `github`. * Maximum of 100 fee earners (including the creator) per token launch. **Endpoint Updates (7-day deprecation):** * `/fee-share/wallet/v2` replaces `/fee-share/wallet/twitter` (now supporting GitHub, Kick, TikTok, Twitter). * `/token-launch/creator/v3` replaces `/token-launch/creator/v2` (response type includes more details). * Old endpoints will be removed in **7 days**. **API Enhancements:** * Optional tip support on launch-related endpoints. Add an optional tip using `tipWallet` (Base58 encoded Solana public key) and `tipLamports` (lamports) on:\ `/token-launch/create-config`, `/token-launch/create-launch-transaction`, `/token-launch/fee-share/create-config`. * No mandatory IPFS upload for token info creation. `/token-launch/create-token-info` now accepts `imageUrl` and/or `metadataUrl`. When `metadataUrl` is provided, we skip IPFS upload and use the provided URL as-is. **SDK v1.0.8:** * Adds `getLaunchWalletV2`. * `getAllClaimablePositions` is more efficient, with new `chunkSize` arg to reduce RPC rate limits. * All downstream functions now support **commitment**. * General performance & stability improvements. **Other Improvements:** * Total fees earned reporting is now more accurate. * No more fees for token creation via API (only pay Solana tx cost). * Better error responses across the board. # CLI Command Reference Source: https://docs.bags.fm/cli/command-reference Complete reference for every Bags CLI command, subcommand, flag, and option This page documents every command available in the Bags CLI. All commands support the global `--json` flag for machine-readable output and `--input-json ` for scripting. Make sure you've completed the [Install and Set Up the Bags CLI](/cli/install-and-setup) guide before using these commands. ## setup First-run wizard that configures RPC, imports your wallet, and authenticates in one step using either `wallet` (default) or `manual` auth mode. ```bash theme={null} bags setup [options] ``` | Flag | Description | Default | | ------------------------------ | ---------------------------------------- | ------------------------------------- | | `--rpc-url ` | Solana RPC URL | `https://api.mainnet-beta.solana.com` | | `--private-key ` | Private key (base58 or integer array) | Prompted | | `--auth-mode ` | Authentication mode | `wallet` | | `--api-key ` | API key to use when `--auth-mode manual` | Prompted in interactive mode | | `--key-name ` | Label for the generated API key | `Bags CLI Key` | ```bash theme={null} bags setup --rpc-url https://my-rpc.com --private-key YOUR_KEY ``` ```bash theme={null} bags setup --private-key YOUR_KEY --auth-mode manual --api-key YOUR_PUBLIC_API_KEY ``` *** ## auth Agent authentication commands. ### auth login Authenticate and store credentials locally. Supports `wallet` mode (challenge/signature flow) and `manual` mode (validate provided API key via `sdk.auth.me()`). ```bash theme={null} bags auth login [options] ``` | Flag | Description | Default | | ------------------------------ | ---------------------------------------- | ----------------------------- | | `--auth-mode ` | Authentication mode | `wallet` | | `--api-key ` | API key to use when `--auth-mode manual` | Prompted in interactive mode | | `--keypair ` | Custom keypair file path | `~/.config/bags/keypair.json` | | `--key-name ` | Label for the API key | `Bags CLI Key` | ```bash theme={null} bags auth login --key-name "Production Key" ``` ```bash theme={null} bags auth login --auth-mode manual --api-key YOUR_PUBLIC_API_KEY ``` ```bash theme={null} bags auth login --input-json '{"authMode":"manual","apiKey":"YOUR_PUBLIC_API_KEY"}' ``` In non-interactive environments, `manual` mode requires `--api-key` (or `--input-json` with `apiKey`) and exits immediately if missing. ### auth status Display the current authentication state including `authMode` and the masked API key. ```bash theme={null} bags auth status ``` ### auth logout Remove stored credentials. Optionally delete the local keypair too. ```bash theme={null} bags auth logout [options] ``` | Flag | Description | | ------- | ------------------------------------ | | `--all` | Also delete the local wallet keypair | ```bash theme={null} bags auth logout --all ``` *** ## wallet Wallet management commands. ### wallet generate Generate and save a new Solana keypair to `~/.config/bags/keypair.json`. ```bash theme={null} bags wallet generate [options] ``` | Flag | Description | | --------- | ---------------------------------- | | `--force` | Overwrite an existing keypair file | ### wallet import Import a keypair from a base58 private key or a JSON secret key file. ```bash theme={null} bags wallet import [options] ``` | Flag | Description | | ---------------- | ----------------------------------------------- | | `--key ` | Base58-encoded private key | | `--file ` | Path to a JSON file containing secret key bytes | ```bash theme={null} bags wallet import --key YOUR_BASE58_PRIVATE_KEY ``` ### wallet show Show the wallet's public key and SOL balance. ```bash theme={null} bags wallet show [options] ``` | Flag | Description | | ------------- | ------------------------------- | | `--rpc ` | Override the configured RPC URL | ### wallet balance Show SOL balance, or a specific SPL token balance. ```bash theme={null} bags wallet balance [options] ``` | Flag | Description | | ---------------- | ------------------------------------------- | | `--rpc ` | Override the configured RPC URL | | `--token ` | SPL token mint address to check balance for | ```bash theme={null} bags wallet balance --token EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v ``` *** ## settings CLI configuration commands. ### settings show Print the current CLI settings (RPC URL, commitment, output mode). ```bash theme={null} bags settings show ``` ### settings set Update one or more CLI settings. ```bash theme={null} bags settings set [options] ``` | Flag | Description | Values | | ---------------------- | ---------------------------- | ------------------------------------- | | `--rpc-url ` | Default Solana RPC endpoint | Any valid URL | | `--commitment ` | Transaction commitment level | `processed`, `confirmed`, `finalized` | | `--output ` | Default output format | `pretty`, `table`, `json` | ```bash theme={null} bags settings set --rpc-url https://my-rpc.com --commitment confirmed --output json ``` *** ## trade Quote and execute token swaps. ### trade quote Get a swap quote without executing a transaction. ```bash theme={null} bags trade quote [options] ``` | Flag | Description | Default | | ------------------------- | --------------------------------------------------------------- | -------- | | `--input-mint
` | Input token mint address | Prompted | | `--output-mint
` | Output token mint address | Prompted | | `--amount ` | Amount in base units (e.g. lamports) | Prompted | | `--slippage-mode ` | `auto` or `manual` | `auto` | | `--slippage-bps ` | Slippage tolerance in basis points (required for `manual` mode) | -- | ```bash theme={null} bags trade quote \ --input-mint So11111111111111111111111111111111 \ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --amount 1000000000 ``` ### trade swap Get a quote, then sign and send the swap transaction. ```bash theme={null} bags trade swap [options] ``` Accepts all the same flags as `trade quote`, plus: | Flag | Description | | ---------------- | ---------------------------------------- | | `--skip-confirm` | Skip the interactive confirmation prompt | ```bash theme={null} bags trade swap \ --input-mint So11111111111111111111111111111111 \ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --amount 1000000000 \ --skip-confirm ``` *** ## launch Token launch flows. ### launch create Create and launch a new token. Handles metadata upload, fee share configuration, lookup table creation, Jito bundles, and the launch transaction automatically. ```bash theme={null} bags launch create [options] ``` | Flag | Description | Default | | --------------------------- | -------------------------------------- | ----------------------------- | | `--name ` | Token name | Prompted | | `--symbol ` | Token ticker symbol | Prompted | | `--description ` | Token description | Prompted | | `--image-url ` | URL to the token image | Prompted (choose URL or file) | | `--image ` | Local path to the token image | Prompted (choose URL or file) | | `--twitter ` | Twitter/X URL | Optional | | `--website ` | Website URL | Optional | | `--telegram ` | Telegram URL | Optional | | `--initial-buy ` | Initial buy amount in lamports | `10000000` (0.01 SOL) | | `--fee-claimers ` | JSON array of fee claimers (see below) | Interactive builder | | `--partner ` | Partner wallet address | -- | | `--partner-config ` | Partner config PDA | -- | | `--skip-confirm` | Skip the final confirmation prompt | -- | **Fee claimers JSON format:** Fee claimers can reference social usernames or direct wallet addresses: ```json theme={null} [ {"provider": "twitter", "username": "alice", "bps": 3000}, {"wallet": "WALLET_PUBKEY_HERE", "bps": 2000} ] ``` The creator automatically receives the remaining BPS (in the example above, 50%). ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "A great token" \ --image-url "https://example.com/image.png" \ --initial-buy 10000000 \ --skip-confirm ``` ### launch feed Get the recent token launch feed. ```bash theme={null} bags launch feed [options] ``` | Flag | Description | Default | | ------------- | ------------------------- | ------- | | `--limit ` | Number of items to return | `20` | ### launch creators Get the creators/deployers for a token. ```bash theme={null} bags launch creators [options] ``` | Flag | Description | | ------------------ | ------------------ | | `--mint
` | Token mint address | ```bash theme={null} bags launch creators --mint TOKEN_MINT_ADDRESS ``` *** ## fees Fee claiming and analytics. ### fees list List all claimable fee positions for your local wallet. ```bash theme={null} bags fees list ``` Outputs a table with columns: Mint, Claimable Lamports, Custom Vault. ### fees claim Claim fees for a single token. ```bash theme={null} bags fees claim [mint] [options] ``` | Flag | Description | | ------------------ | ----------------------------------------------- | | `--mint
` | Token mint (alternative to positional argument) | | `--skip-confirm` | Skip the confirmation prompt | ```bash theme={null} bags fees claim TOKEN_MINT_ADDRESS --skip-confirm ``` ### fees claim-all Claim all available fees across every token. ```bash theme={null} bags fees claim-all [options] ``` | Flag | Description | | ---------------- | ---------------------------- | | `--skip-confirm` | Skip the confirmation prompt | ### fees lifetime Get total lifetime fees collected for a token. ```bash theme={null} bags fees lifetime [mint] [options] ``` | Flag | Description | | ------------------ | ----------------------------------------------- | | `--mint
` | Token mint (alternative to positional argument) | | `--raw` | Show raw lamports instead of formatted SOL | ```bash theme={null} bags fees lifetime TOKEN_MINT_ADDRESS --raw ``` ### fees events Get claim event history for a token. Supports pagination via offset/limit or time-range filtering. ```bash theme={null} bags fees events [mint] [options] ``` | Flag | Description | | -------------------- | ----------------------------------------------- | | `--mint
` | Token mint (alternative to positional argument) | | `--offset ` | Pagination offset | | `--limit ` | Number of events to return | | `--start-time ` | Start timestamp (ISO 8601) | | `--end-time ` | End timestamp (ISO 8601) | ```bash theme={null} bags fees events TOKEN_MINT --offset 0 --limit 10 bags fees events TOKEN_MINT --start-time 2025-01-01T00:00:00Z --end-time 2025-06-01T00:00:00Z ``` ### fees stats Get aggregated claim statistics for a token. ```bash theme={null} bags fees stats [mint] [options] ``` | Flag | Description | | ------------------ | ----------------------------------------------- | | `--mint
` | Token mint (alternative to positional argument) | *** ## config Fee share configuration management (admin operations). ### config create Create a fee share configuration for a token. ```bash theme={null} bags config create [options] ``` | Flag | Description | | ----------------------- | --------------------------------------------------- | | `--mint
` | Base token mint | | `--fee-claimers ` | JSON array of `[{"user":"PUBKEY","userBps":10000}]` | | `--partner ` | Partner wallet address | | `--skip-confirm` | Skip the confirmation prompt | ```bash theme={null} bags config create \ --mint TOKEN_MINT \ --fee-claimers '[{"user":"WALLET_A","userBps":5000},{"user":"WALLET_B","userBps":5000}]' \ --skip-confirm ``` ### config update Update an existing fee share configuration (requires admin authority). ```bash theme={null} bags config update [options] ``` | Flag | Description | | ----------------------- | --------------------------------------------------- | | `--mint
` | Base token mint | | `--fee-claimers ` | New JSON array of `[{"user":"PUBKEY","userBps":N}]` | | `--skip-confirm` | Skip the confirmation prompt | ### config transfer-admin Transfer fee share admin authority to a new wallet. ```bash theme={null} bags config transfer-admin [options] ``` | Flag | Description | | ---------------------- | ---------------------------- | | `--mint
` | Base token mint | | `--new-admin ` | New admin wallet address | | `--skip-confirm` | Skip the confirmation prompt | ```bash theme={null} bags config transfer-admin --mint TOKEN_MINT --new-admin NEW_ADMIN_PUBKEY ``` ### config admin-list List all token mints where your wallet is the fee share admin. ```bash theme={null} bags config admin-list ``` *** ## partner Partner configuration and fee claiming. ### partner create Create a partner configuration for your local wallet. Each wallet can only have one partner config. ```bash theme={null} bags partner create [options] ``` | Flag | Description | | ---------------- | ---------------------------- | | `--skip-confirm` | Skip the confirmation prompt | ### partner stats Get statistics for a partner wallet. ```bash theme={null} bags partner stats [options] ``` | Flag | Description | | -------------------- | ------------------------- | | `--partner ` | Partner wallet public key | ```bash theme={null} bags partner stats --partner PARTNER_PUBKEY ``` ### partner claim Claim accumulated partner fees. ```bash theme={null} bags partner claim [options] ``` | Flag | Description | | ---------------- | ---------------------------- | | `--skip-confirm` | Skip the confirmation prompt | *** ## pool Pool lookup commands. ### pool list List Bags liquidity pools. ```bash theme={null} bags pool list [options] ``` | Flag | Description | Default | | -------------- | ------------------------- | ------- | | `--limit ` | Number of pools to return | `50` | | `--offset ` | Pagination offset | `0` | ### pool get Get details for a specific pool by token mint. ```bash theme={null} bags pool get [options] ``` | Flag | Description | | ------------------ | ------------------ | | `--mint
` | Token mint address | ```bash theme={null} bags pool get --mint TOKEN_MINT_ADDRESS ``` *** ## dexscreener Dexscreener listing order flows. ### dexscreener check Check if a Dexscreener order is available for a token. ```bash theme={null} bags dexscreener check [options] ``` | Flag | Description | | ------------------ | ------------------ | | `--mint
` | Token mint address | ### dexscreener order Create a Dexscreener listing order. ```bash theme={null} bags dexscreener order [options] ``` | Flag | Description | | ------------------ | ------------------------- | | `--mint
` | Token mint address | | `--payload ` | Raw JSON payload override | ### dexscreener pay Submit payment for a Dexscreener order. ```bash theme={null} bags dexscreener pay [options] ``` | Flag | Description | | -------------------- | ------------------------------------ | | `--order-id ` | Order ID from the `order` command | | `--transaction ` | Signed serialized transaction string | # Install and Set Up the Bags CLI Source: https://docs.bags.fm/cli/install-and-setup Install the Bags CLI, configure your wallet and RPC, authenticate, and start using terminal-based workflows The Bags CLI (`@bagsfm/bags-cli`) is a terminal tool that wraps the Bags API and SDK into simple commands. It lets you trade tokens, launch tokens, claim fees, manage fee share configs, and more all without writing code. ## Requirements * **Node.js 18** or later Verify your Node version: ```bash theme={null} node --version ``` ## Installation Install globally via npm: ```bash theme={null} npm install -g @bagsfm/bags-cli ``` Or run directly without installing: ```bash theme={null} npx @bagsfm/bags-cli --help ``` After installing globally, the `bags` binary is available in your terminal: ```bash theme={null} bags --help ``` ## Quick Start with `bags setup` The fastest way to get going is the first-run wizard. It configures your RPC endpoint, imports your wallet, and authenticates in a single command: ```bash theme={null} bags setup ``` The wizard prompts you for: 1. **Solana RPC URL** -- defaults to `https://api.mainnet-beta.solana.com` if left blank 2. **Private key** -- accepts a base58-encoded key or a JSON integer array 3. **Auth mode** -- `wallet` (default) or `manual` 4. **API key** -- only when using `manual` mode 5. **MFA code** -- only in `wallet` mode if your account has MFA enabled You can also pass everything as flags for non-interactive use: ```bash theme={null} bags setup \ --rpc-url https://your-rpc-provider.com \ --private-key YOUR_BASE58_PRIVATE_KEY \ --key-name "My CLI Key" ``` Manual mode example: ```bash theme={null} bags setup \ --rpc-url https://your-rpc-provider.com \ --private-key YOUR_BASE58_PRIVATE_KEY \ --auth-mode manual \ --api-key YOUR_PUBLIC_API_KEY ``` | Flag | Description | Default | | ------------------------------ | ---------------------------------------- | ------------------------------------- | | `--rpc-url ` | Solana RPC endpoint | `https://api.mainnet-beta.solana.com` | | `--private-key ` | Base58 private key or JSON integer array | Prompted interactively | | `--auth-mode ` | Authentication mode | `wallet` | | `--api-key ` | API key to use when `--auth-mode manual` | Prompted in interactive mode | | `--key-name ` | Label for the generated API key | `Bags CLI Key` | In non-interactive environments, `manual` mode fails fast if no API key is provided (via `--api-key` or `--input-json`). Once setup completes, you'll see a summary with your wallet address, masked API key, auth mode, and RPC URL. ## Manual Setup (Step by Step) If you prefer to configure each piece individually: ### 1. Create or Import a Wallet Generate a new Solana keypair: ```bash theme={null} bags wallet generate ``` Or import an existing key: ```bash Base58 key theme={null} bags wallet import --key YOUR_BASE58_PRIVATE_KEY ``` ```bash JSON file theme={null} bags wallet import --file /path/to/keypair.json ``` The keypair is saved to `~/.config/bags/keypair.json`. ### 2. Authenticate Log in using either auth mode: ```bash theme={null} bags auth login ``` Manual mode example: ```bash theme={null} bags auth login --auth-mode manual --api-key YOUR_PUBLIC_API_KEY ``` If your account has MFA enabled, you'll be prompted for the code in `wallet` mode. | Flag | Description | Default | | ------------------------------ | ---------------------------------------- | ----------------------------- | | `--auth-mode ` | Authentication mode | `wallet` | | `--api-key ` | API key to use when `--auth-mode manual` | Prompted in interactive mode | | `--keypair ` | Path to a custom keypair file | `~/.config/bags/keypair.json` | | `--key-name ` | Label for the API key | `Bags CLI Key` | ### 3. Configure Settings Set your preferred RPC endpoint and commitment level: ```bash theme={null} bags settings set --rpc-url https://your-rpc-provider.com --commitment confirmed ``` ### 4. Verify Check that everything is working: ```bash theme={null} bags auth status bags wallet show ``` ## Configuration Files All CLI state lives under `~/.config/bags/`: | File | Purpose | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `keypair.json` | Your Solana keypair (secret key bytes). Permissions are set to `0600`. | | `credentials.json` | API key, key ID (when available), auth mode, wallet address, and authentication timestamp. Permissions are set to `0600`. | | `config.json` | CLI settings: RPC URL, commitment level, output mode. | Never share your `keypair.json` or `credentials.json`. These files contain secrets that grant full access to your wallet and API key. ## Global Options Every command supports these global flags: | Flag | Description | | --------------------- | ---------------------------------------------------------------- | | `--json` | Force machine-readable JSON output regardless of settings | | `--input-json ` | Provide options as a JSON object, merged with any explicit flags | The `--input-json` flag is useful for scripting. Explicit CLI flags take precedence over values in the JSON object: ```bash theme={null} bags trade quote --input-json '{"inputMint":"So11...","outputMint":"EPjF...","amount":1000000}' ``` ## Output Modes Control how the CLI formats its output: ```bash theme={null} bags settings set --output json ``` | Mode | Description | | -------- | --------------------------------------- | | `pretty` | Human-friendly colored output (default) | | `table` | Tabular format for list data | | `json` | Raw JSON for piping into other tools | View your current settings at any time: ```bash theme={null} bags settings show ``` ## Uninstalling Remove credentials and keypair: ```bash theme={null} bags auth logout --all ``` Uninstall the package: ```bash theme={null} npm uninstall -g @bagsfm/bags-cli ``` ## Next Steps * Browse the full [CLI Command Reference](/cli/command-reference) to see every available command. * Follow any of the [How-to Guides](/how-to-guides/trade-tokens) each guide includes CLI alternatives alongside the SDK examples. # Quickstart: Launch a Token Source: https://docs.bags.fm/cli/quickstart-token-launch Go from zero to a live token on Solana in three commands using the Bags CLI This guide shows the fastest path from an empty terminal to a launched token. Three commands, no code. ## Prerequisites * **Node.js 18+** installed ([download](https://nodejs.org)) * A **Solana wallet private key** (base58-encoded, exportable from Bags, Phantom, or Backpack) * Some **SOL** in the wallet for transaction fees and the initial buy ## Step 1 -- Install the CLI ```bash theme={null} npm install -g @bagsfm/bags-cli ``` ## Step 2 -- Set up your wallet and authenticate Run the setup wizard. It configures your RPC, imports your wallet, and authenticates all in one command: ```bash theme={null} bags setup --rpc-url YOUR_RPC_URL --private-key YOUR_BASE58_PRIVATE_KEY ``` If you leave `--rpc-url` blank, it defaults to `https://api.mainnet-beta.solana.com`. For production launches, use a dedicated RPC provider for better reliability. `bags setup` uses `wallet` auth mode by default. If you already have an API key, use `--auth-mode manual --api-key YOUR_PUBLIC_API_KEY`. You'll see a summary once setup completes: ``` Setup complete! Wallet: YourWa11etPubkeyHere... API Key: abc123...wxyz RPC: https://your-rpc-provider.com ``` ## Step 3 -- Launch your token ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "My first token on Bags" \ --image-url "https://example.com/logo.png" \ --initial-buy 10000000 \ --skip-confirm ``` That's it. The CLI handles metadata upload, fee share configuration, Jito bundle tips, and the launch transaction automatically. The `--initial-buy` value is in **lamports** (1 SOL = 1,000,000,000 lamports). The example above uses 10,000,000 lamports = 0.01 SOL. Once the launch succeeds, you'll see output like: ``` tokenMint: AbC123...xYz metadataUrl: https://arweave.net/... configKey: DeF456...uVw signature: 5KtPn1LGux... ``` View your token at `https://bags.fm/YOUR_TOKEN_MINT`. *** ## The full sequence ```bash theme={null} # 1. Install npm install -g @bagsfm/bags-cli # 2. Setup (wallet + auth) bags setup --rpc-url YOUR_RPC_URL --private-key YOUR_BASE58_PRIVATE_KEY # 3. Launch bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "My first token on Bags" \ --image-url "https://example.com/logo.png" \ --initial-buy 10000000 \ --skip-confirm ``` ## Optional: Share fees with others Add `--fee-claimers` to split fees with other users by social handle or wallet address: ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "A token with shared fees" \ --image-url "https://example.com/logo.png" \ --initial-buy 10000000 \ --fee-claimers '[{"provider":"twitter","username":"ramyobags","bps":3000}]' \ --skip-confirm ``` The creator automatically keeps the remaining share (70% in this example). See the [Launch a Token](/how-to-guides/launch-token) guide for the full fee sharing details. ## Optional: Use a local image file If you have the image on disk instead of a URL: ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "My first token on Bags" \ --image /path/to/logo.png \ --initial-buy 10000000 \ --skip-confirm ``` ## Optional: Interactive mode Omit the flags and the CLI walks you through each field interactively: ```bash theme={null} bags launch create ``` You'll be prompted for the token name, symbol, description, image, initial buy amount, and whether to share fees -- step by step. ## What's next? * [Claim fees](/how-to-guides/claim-fees) earned from your token: `bags fees claim TOKEN_MINT` * [Trade tokens](/how-to-guides/trade-tokens): `bags trade swap --input-mint ... --output-mint ... --amount ...` * [CLI Command Reference](/cli/command-reference) for every available command * [Install and Set Up the Bags CLI](/cli/install-and-setup) for detailed configuration options # Do I need a Solana wallet to use the API? Source: https://docs.bags.fm/faq/do-i-need-wallet Learn about wallet requirements for using the Bags API ## Do I need a Solana wallet to use the API? For operations that require on-chain transactions like creating a token or claiming fees you'll need a Solana wallet with enough SOL to cover transaction costs, but you don't necessarily need SOL for non-transactional usage. ### Wallet Requirements * **Token Launch**: Requires a wallet with SOL for transaction fees * **Fee Sharing**: Needs wallet access for configuring and claiming fees * **Transactions**: Any blockchain operations require wallet signing ### Getting Started 1. Export your wallet's private key (base58 encoded) 2. Add it to your environment variables: `PRIVATE_KEY=your_base58_encoded_private_key` 3. Use it in your scripts to sign transactions You can export your private key from wallets like Bags, Phantom, or Backpack. See our [Token Launch Guide](/how-to-guides/launch-token) for examples. ### Security Best Practices * Never commit private keys to version control * Use environment variables or secure secret management * Consider using a dedicated development wallet # How do I get an API key? Source: https://docs.bags.fm/faq/how-to-get-api-key Step-by-step guide to obtaining your Bags API key ## How do I get an API key? Getting an API key is simple and only takes a few minutes. ### Steps to Get Your API Key 1. Visit [dev.bags.fm](https://dev.bags.fm) and sign in to your account 2. Navigate to the **API Keys** section 3. Click **Generate new Key** 4. Give your key a descriptive name (e.g., "Production API", "Development API") 5. Copy and securely store your API key Keep your API keys secure and never share them publicly. Each user can create up to 10 API keys. ### Using Your API Key Include your API key in the `x-api-key` header with every request: ```bash theme={null} curl -X GET 'https://public-api-v2.bags.fm/api/v1/endpoint' \ -H 'x-api-key: YOUR_API_KEY' ``` ### Managing API Keys You can revoke API keys at any time from the [Developer Portal](https://dev.bags.fm). Revoking a key immediately stops all requests using that key. # How to display social profile on token page? Source: https://docs.bags.fm/faq/linking-wallets Learn how to link your wallet to Bags to display your identity when creating tokens via API ## How to display social profile on token page? When launching your token via API, you can choose any wallet address.\ However, the **creator identity shown on the token page** depends on the wallet you use: If you use **any external wallet address**, your token page will display only the wallet address. External Wallet Example Use your **Bags wallet** (export the private key from [Bags.fm](https://bags.fm)) when creating tokens via API. Your token page will display your **social profiles** (X, GitHub, Kick, TikTok) instead of just a wallet address. Bags Wallet Example ## How to Export Your Bags Wallet to Phantom Follow these simple steps to use your Bags wallet externally (e.g., with the API or Phantom): 1. Visit [bags.fm](https://bags.fm) 2. Log in with your credentials 3. Tap your **username / profile picture** in the top-right corner 4. Select **My Wallets** → **Export Private Key** for the wallet you want to export\ *(Do this only in a safe, private environment)* 5. Tap **Copy Key** to copy the private key to your clipboard 6. You can now import your private key into any wallet app (such as Phantom), or use it in your API scripts to launch a token.
  - To launch a token with your exported wallet, follow our [Token Launch Guide](/how-to-guides/launch-token). Keep your private key safe — store it offline (e.g., written on paper or in a password manager).\ Never share it publicly or in untrusted apps. If you encounter any issues, please contact the Bags team via support or your Bags dashboard. # What are the rate limits? Source: https://docs.bags.fm/faq/what-are-rate-limits Understanding Bags API rate limits and how to monitor usage ## What are the rate limits? The Bags API implements rate limiting to ensure fair usage and system stability. ### Rate Limit Details * **Limit**: 5,000 requests per hour per user and per ip * **Scope**: Rate limits apply across all your API keys (shared quota) * **Headers**: Response headers include rate limit information ### Monitoring Your Usage Check these response headers to monitor your API usage: * `X-RateLimit-Limit`: Total requests allowed per hour (5,000) * `X-RateLimit-Remaining`: Requests remaining in current window * `X-RateLimit-Reset`: Unix timestamp when the limit resets ### Example ```javascript theme={null} const response = await fetch('https://public-api-v2.bags.fm/api/v1/endpoint', { headers: { 'x-api-key': 'YOUR_API_KEY' } }); console.log('Remaining:', response.headers.get('X-RateLimit-Remaining')); console.log('Resets at:', new Date( parseInt(response.headers.get('X-RateLimit-Reset')) * 1000 )); ``` Distribute requests evenly throughout the hour to avoid hitting rate limits. Consider implementing exponential backoff for failed requests. See the [Rate Limits guide](/principles/rate-limits) for more detailed information. # What is the Bags API? Source: https://docs.bags.fm/faq/what-is-bags-api Learn about the Bags API and what it allows you to do ## What is the Bags API? The Bags API allows you to integrate Bags functionality into your applications. You can launch tokens, manage fee shares, and retrieve analytics data programmatically. ### Key Features * **Token Launch**: Create and launch Solana tokens through the API * **Fee Sharing**: Configure fee sharing between creators and fee claimers * **Analytics**: Retrieve token lifetime fees and creator information * **State Management**: Query pool configurations and token data ### Getting Started To start using the Bags API: 1. Get your API key from the [Bags Developer Portal](https://dev.bags.fm) 2. Review the [API Reference](/api-reference/introduction) documentation 3. Follow our [TypeScript Setup Guide](/how-to-guides/typescript-node-setup) to get started quickly # Agent Authentication and Skill Workflow Source: https://docs.bags.fm/how-to-guides/agent-authentication How AI agents authenticate with wallet signatures, obtain API keys, and use the Bags skill endpoint workflow This guide explains the full Agent V2 authentication flow and how the Bags skill uses your credentials to run fee claiming, trading, and token launch workflows. ## Prerequisites Before starting, make sure you have: * Node.js 18+ and npm * `curl` and `jq` * A local Solana keypair file for your agent * Dependencies for signing: ```bash theme={null} npm install @solana/web3.js bs58 tweetnacl ``` ## Endpoints Used in This Guide * [`POST /agent/v2/auth/init`](/api-reference/agent-auth-init) * [`POST /agent/v2/auth/callback`](/api-reference/agent-auth-callback) ## 1. Create or Load an Agent Wallet Create a local keypair (once), then print your wallet address. ```bash theme={null} mkdir -p ~/.config/bags node -e ' const fs = require("fs"); const { Keypair } = require("@solana/web3.js"); const keypairPath = `${process.env.HOME}/.config/bags/keypair.json`; if (!fs.existsSync(keypairPath)) { const kp = Keypair.generate(); fs.writeFileSync(keypairPath, JSON.stringify(Array.from(kp.secretKey))); } const secret = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf8"))); const wallet = Keypair.fromSecretKey(secret).publicKey.toBase58(); process.stdout.write(wallet); ' chmod 600 ~/.config/bags/keypair.json ``` Save the address as `BAGS_WALLET`: ```bash theme={null} BAGS_WALLET=$(node -e ' const fs = require("fs"); const { Keypair } = require("@solana/web3.js"); const keypairPath = `${process.env.HOME}/.config/bags/keypair.json`; const secret = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf8"))); process.stdout.write(Keypair.fromSecretKey(secret).publicKey.toBase58()); ') ``` ## 2. Initialize Authentication Challenge Request a challenge message and nonce: ```bash theme={null} INIT_RESPONSE=$(curl -s -X POST "https://public-api-v2.bags.fm/api/v1/agent/v2/auth/init" \ -H "Content-Type: application/json" \ -d "{\"address\":\"$BAGS_WALLET\"}") echo "$INIT_RESPONSE" | jq ``` Expected shape: ```json theme={null} { "success": true, "response": { "message": "", "nonce": "" } } ``` ## 3. Sign the Challenge Message The `message` returned by init is base58-encoded. Decode it to bytes, sign using your Ed25519 key, then base58-encode the signature. ```bash theme={null} CHALLENGE_MESSAGE=$(echo "$INIT_RESPONSE" | jq -r '.response.message') CHALLENGE_NONCE=$(echo "$INIT_RESPONSE" | jq -r '.response.nonce') SIGNATURE=$(node -e ' const fs = require("fs"); const bs58mod = require("bs58"); const bs58 = bs58mod.default || bs58mod; const nacl = require("tweetnacl"); const keypairPath = `${process.env.HOME}/.config/bags/keypair.json`; const messageB58 = process.argv[1]; const secret = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf8"))); const messageBytes = bs58.decode(messageB58); const signatureBytes = nacl.sign.detached(messageBytes, secret); process.stdout.write(bs58.encode(signatureBytes)); ' "$CHALLENGE_MESSAGE") ``` ## 4. Complete Signature Callback Send the signature payload: ```bash theme={null} CALLBACK_RESPONSE=$(curl -s -X POST "https://public-api-v2.bags.fm/api/v1/agent/v2/auth/callback" \ -H "Content-Type: application/json" \ -d "{ \"signature\": \"$SIGNATURE\", \"address\": \"$BAGS_WALLET\", \"nonce\": \"$CHALLENGE_NONCE\", \"keyName\": \"My Agent Key\" }") echo "$CALLBACK_RESPONSE" | jq ``` Two outcomes are possible: 1. API key returned immediately 2. MFA required (`mfaRequired: true`) and `authCode` returned ## 5. Handle MFA Callback (If Required) If callback returns `mfaRequired: true`, call the same endpoint again with your MFA code: ```bash theme={null} AUTH_CODE=$(echo "$CALLBACK_RESPONSE" | jq -r '.response.authCode') MFA_CODE="123456" MFA_RESPONSE=$(curl -s -X POST "https://public-api-v2.bags.fm/api/v1/agent/v2/auth/callback" \ -H "Content-Type: application/json" \ -d "{ \"authCode\": \"$AUTH_CODE\", \"mfaCode\": \"$MFA_CODE\", \"keyName\": \"My Agent Key\" }") echo "$MFA_RESPONSE" | jq ``` ## 6. Store Credentials Securely Save returned credentials in a local file: ```bash theme={null} API_KEY=$(echo "$CALLBACK_RESPONSE" | jq -r '.response.apiKey // empty') KEY_ID=$(echo "$CALLBACK_RESPONSE" | jq -r '.response.keyId // empty') if [ -z "$API_KEY" ]; then API_KEY=$(echo "$MFA_RESPONSE" | jq -r '.response.apiKey') KEY_ID=$(echo "$MFA_RESPONSE" | jq -r '.response.keyId') fi mkdir -p ~/.config/bags cat > ~/.config/bags/credentials.json << EOF { "api_key": "$API_KEY", "key_id": "$KEY_ID", "wallet_address": "$BAGS_WALLET", "wallet_keypair_path": "$HOME/.config/bags/keypair.json", "authenticated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" } EOF chmod 600 ~/.config/bags/credentials.json ``` ## How the Skill Works After Authentication Once `api_key` is stored, the skill follows a consistent pattern: 1. Read `~/.config/bags/credentials.json` 2. Call a domain endpoint with `x-api-key` 3. If a transaction is returned, sign with local keypair 4. Submit through [`POST /solana/send-transaction`](/api-reference/send-transaction) ### Skill Modules and Their Endpoints | Skill module | Purpose | Main endpoints | | ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AUTH.md | Wallet-signature auth + MFA | `/agent/v2/auth/init`, `/agent/v2/auth/callback` | | FEES.md | Discover and claim earnings | [`GET /token-launch/claimable-positions`](/api-reference/get-claimable-positions), [`POST /token-launch/claim-txs/v3`](/api-reference/get-claim-transactions-v3) | | TRADING.md | Quote and swap tokens | [`GET /trade/quote`](/api-reference/get-trade-quote), [`POST /trade/swap`](/api-reference/create-swap-transaction) | | LAUNCH.md | Create token metadata and launch tx | [`POST /token-launch/create-token-info`](/api-reference/create-token-info), [`POST /fee-share/config`](/api-reference/create-fee-share-configuration), [`POST /token-launch/create-launch-transaction`](/api-reference/create-token-launch-transaction) | | WALLETS.md | Local wallet ops and signing | Uses API tx endpoints plus local signer script | | HEARTBEAT.md | Periodic health checks | Reuses claimable positions + optional claim/trade flows | ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. The CLI handles authentication in two modes: * `wallet` (default): init/sign/callback (+ MFA when required) * `manual`: validate a provided API key via `sdk.auth.me()` Instead of writing scripts with `curl` and `jq`, you can authenticate in a single command: **Quick setup (wallet + auth in one step):** ```bash theme={null} bags setup --private-key YOUR_BASE58_PRIVATE_KEY --key-name "My Agent Key" ``` **Quick setup (manual API key mode):** ```bash theme={null} bags setup --private-key YOUR_BASE58_PRIVATE_KEY --auth-mode manual --api-key YOUR_PUBLIC_API_KEY ``` **Or step by step:** ```bash theme={null} # 1. Import your wallet bags wallet import --key YOUR_BASE58_PRIVATE_KEY # 2. Authenticate (handles challenge, signature, and MFA automatically) bags auth login --key-name "My Agent Key" # 3. Verify authentication bags auth status ``` Manual step-by-step variant: ```bash theme={null} bags auth login --auth-mode manual --api-key YOUR_PUBLIC_API_KEY bags auth status ``` **Manage credentials:** ```bash theme={null} # Log out (remove API key) bags auth logout # Log out and delete the local keypair bags auth logout --all ``` The CLI stores credentials in `~/.config/bags/credentials.json` with `0600` permissions and includes `authMode` metadata (`wallet` or `manual`). Existing credentials without `authMode` are treated as `wallet`. ## Security and Reliability Notes * Nonces are single-use and expire quickly (re-run init if expired). * Never expose secret key bytes in logs. * Keep keypair and credentials files at `chmod 600`. * API key is shown once on successful callback; store it immediately. * For retries, regenerate a fresh nonce and signature rather than replaying old payloads. ## Common Errors * `Nonce not found or expired`: run init again, sign the new message, retry callback. * `Invalid signature`: ensure you sign decoded message bytes, not the plain base58 string. * `Invalid or expired auth code`: rerun signature callback to get a fresh `authCode`. * `Too many requests`: wait and retry (auth endpoints are rate-limited). ## Related Skill Sources * [Bags skill repository](https://github.com/bagsfm/bags-skill) * [Bags skill entrypoint (SKILL.md)](https://bags.fm/SKILL.md) * [Bags skill metadata (skill.json)](https://bags.fm/skill.json) # Claim Token Fees Source: https://docs.bags.fm/how-to-guides/claim-fees Complete guide to claim fees for a token using the Bags API with TypeScript and Node.js In this guide, you'll learn how to claim fees for a specific token using the Bags TypeScript SDK with Node.js. The simplified claim flow only requires a token mint address and your wallet — the API handles the rest. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with claimable fees (from token launches, liquidity pools, etc.). * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` ## 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ## 2. The Fee Claiming Script Here is a script to claim fees for a specific token. You can save this as `claim-fees.ts`. The claim flow is straightforward — pass your wallet and the token mint to the SDK and you'll receive ready-to-sign transactions back. ## Endpoints Used Under the Hood This guide uses these API endpoints via the SDK: * [`POST /token-launch/claim-txs/v3`](/api-reference/get-claim-transactions-v3) via `sdk.fee.getClaimTransactions()` * [`GET /token-launch/claimable-positions`](/api-reference/get-claimable-positions) via `sdk.fee.getAllClaimablePositions()` ```typescript theme={null} // claim-fees.ts import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, signAndSendTransaction } from "@bagsfm/bags-sdk"; import { Keypair, Connection, PublicKey } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function claimFeesForToken(tokenMint: string) { try { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); const commitment = sdk.state.getCommitment(); console.log(`Claiming fees for token ${tokenMint} with wallet ${keypair.publicKey.toBase58()}`); // Get claim transactions — just pass your wallet and token mint const claimTransactions = await sdk.fee.getClaimTransactions( keypair.publicKey, new PublicKey(tokenMint) ); if (!claimTransactions || claimTransactions.length === 0) { console.log("No claimable fees found for this token."); return; } console.log(`Generated ${claimTransactions.length} claim transaction(s)`); // Sign and send each transaction for (let i = 0; i < claimTransactions.length; i++) { const transaction = claimTransactions[i]; try { const signature = await signAndSendTransaction( connection, commitment, transaction, keypair ); console.log(`Transaction ${i + 1} confirmed: ${signature}`); } catch (txError) { console.error(`Failed to send transaction ${i + 1}:`, txError); } } console.log("Fee claiming completed!"); } catch (error) { console.error("Unexpected error:", error); } } claimFeesForToken("TOKEN_MINT"); ``` ## 3. How It Works The SDK's `getClaimTransactions()` method handles all the complexity for you: 1. **You provide**: Your wallet and the token mint 2. **The API automatically**: Looks up your claimable position, determines the position type (virtual pool, DAMM V2, custom fee vault), and builds the appropriate claim transactions 3. **You receive**: An array of versioned transactions ready to sign and send **Non-SOL (DAMM v2 direct) launches:** Fees for tokens launched directly into a DAMM v2 pool with a non-SOL quote token pay out in that pool's quote mint, and partner/deployer revenue is swept from aggregate vaults. That flow uses `claim-txs/v2` and the vault endpoints instead of the simplified method shown here — see [Launch a Token with a Non-SOL Quote Token](/how-to-guides/launch-token-non-sol-quote). ## 4. Checking Claimable Positions (Optional) If you want to inspect your claimable positions before claiming — for example, to check claimable amounts or see which tokens have fees available — you can use the SDK's `getAllClaimablePositions()` method: ```typescript theme={null} async function getClaimablePositions(wallet: PublicKey) { const positions = await sdk.fee.getAllClaimablePositions(wallet); console.log(`Found ${positions.length} claimable position(s)`); for (const position of positions) { console.log(`\nToken: ${position.baseMint}`); console.log(` Total claimable: ${(position.totalClaimableLamportsUserShare / 1_000_000_000).toFixed(9)} SOL`); console.log(` Custom fee vault: ${position.isCustomFeeVault ? "Yes" : "No"}`); } return positions; } ``` This is useful for building UIs that display fee balances, or for deciding which tokens to claim fees for. ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. The CLI lets you view and claim fees without writing scripts: **List all claimable positions:** ```bash theme={null} bags fees list ``` **Claim fees for a specific token:** ```bash theme={null} bags fees claim TOKEN_MINT_ADDRESS ``` **Claim all available fees across every token:** ```bash theme={null} bags fees claim-all ``` Add `--skip-confirm` to either claim command to bypass the confirmation prompt (useful for automation). ## 5. Running the Script Replace `TOKEN_MINT` with the token mint address you want to claim fees for, then run: ```bash theme={null} npx ts-node claim-fees.ts ``` The script will automatically generate and send claim transactions for the specified token using your wallet. ## 6. Error Handling Common errors you may encounter: * **No claimable fees**: The wallet has no fees to claim for the specified token * **Invalid token mint**: The token mint address is not a valid Solana public key * **Insufficient SOL**: Your wallet needs SOL for transaction fees * **Rate limiting**: Respect API rate limits between requests Check the console output for detailed error messages. # Claim Partner Fees Source: https://docs.bags.fm/how-to-guides/claim-partner-fees Complete guide to claim fees from partner configurations using the Bags Dev Dashboard or SDK In this guide, you'll learn how to check and claim fees from your partner configuration. Partner fees are accumulated from token launches that include your partner configuration. You can claim partner fees using either the Bags Dev Dashboard or the TypeScript SDK. **Transaction Fees**: Partners pay transaction fees for all source claim steps, and the SOL you’re claiming is only received in the final vault-withdraw transaction. Keep enough SOL up front so every claim transaction can succeed and you collect the funds on that last step. ## Method 1: Using the Dev Dashboard The easiest way to check and claim partner fees is through the Bags Developer Dashboard. ### Step 1: Access the Dashboard 1. Go to [https://dev.bags.fm](https://dev.bags.fm) and log in with your account. ### Step 2: View Claim Stats 1. Navigate to the **Partner Key** table in the dashboard. 2. View your unclaimed fees in the table. The table displays: * Your partner config key * Claimed fees * Unclaimed fees Partner Key Table with Stats ### Step 3: Claim Fees 1. If you have unclaimed fees, click the **"Claim"** button next to your partner key in the table. 2. Confirm the transaction in your connected wallet. 3. Your fees will be claimed and transferred to your wallet. Make sure your wallet has sufficient SOL balance to pay for the transaction fees. ## Method 2: Using the SDK You can also check and claim partner fees programmatically using the Bags TypeScript SDK. ### Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with a partner configuration (see [Create Partner Key](/how-to-guides/create-partner-key) guide). * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` ### 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ### 2. The Partner Fee Claiming Script Here is a comprehensive script to check partner fee stats and claim fees. You can save this as `claim-partner-fees.ts`. This script uses the Bags SDK's `partner` service to check claimable fees and generate claim transactions. The SDK returns claim transactions where **the last transaction must be executed last** because it withdraws funds from the user vault. All preceding transactions fund that vault and could be processed in parallel before the final one, but this guide executes them sequentially for simplicity. ### Endpoints Used Under the Hood This SDK flow calls these API endpoints: * [`GET /fee-share/partner-config/stats`](/api-reference/get-partner-stats) via `sdk.partner.getPartnerConfigClaimStats()` * [`POST /fee-share/partner-config/claim-tx`](/api-reference/get-partner-claim-transactions) via `sdk.partner.getPartnerConfigClaimTransactions()` `sdk.partner.getPartnerConfig()` is an on-chain account read (PDA fetch), not a Bags HTTP API endpoint. ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, signAndSendTransaction } from "@bagsfm/bags-sdk"; import { Keypair, PublicKey, Connection } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function claimPartnerFees(partnerWallet: PublicKey) { try { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); console.log(`💰 Checking partner fees for wallet: ${partnerWallet.toBase58()}`); // Check if partner config exists try { const partnerConfig = await sdk.partner.getPartnerConfig(partnerWallet); console.log("✅ Partner config found!"); console.log(` Partner: ${partnerConfig.partner.toBase58()}`); console.log(` BPS: ${partnerConfig.bps}`); console.log(` Total Claimed Fees: ${partnerConfig.totalClaimedFees.toString()}`); console.log(` Total Accumulated Fees: ${partnerConfig.totalAccumulatedFees.toString()}`); } catch (error: any) { if (error.message?.includes("not found")) { console.error("❌ Partner config not found. Please create a partner key first."); console.log(" See the [Create Partner Key](/how-to-guides/create-partner-key) guide."); return; } throw error; } // Get partner claim stats console.log("\n📊 Fetching partner claim stats..."); const stats = await sdk.partner.getPartnerConfigClaimStats(partnerWallet); const claimedFees = BigInt(stats.claimedFees); const unclaimedFees = BigInt(stats.unclaimedFees); console.log(`💰 Claimed Fees: ${claimedFees.toString()} lamports`); console.log(`💰 Unclaimed Fees: ${unclaimedFees.toString()} lamports`); if (unclaimedFees === 0n) { console.log("\n✨ No unclaimed fees available to claim."); return; } const unclaimedFeesSOL = Number(unclaimedFees) / 1_000_000_000; // Convert lamports to SOL console.log(`💵 Unclaimed Fees: ${unclaimedFeesSOL.toFixed(9)} SOL`); // Get claim transactions console.log("\n🎯 Generating claim transactions..."); const claimTransactions = await sdk.partner.getPartnerConfigClaimTransactions(partnerWallet); if (!claimTransactions || claimTransactions.length === 0) { console.log("⚠️ No claim transactions available."); return; } console.log(`✨ Generated ${claimTransactions.length} claim transaction(s)`); // Sign and send transactions // The SDK guarantees the last transaction withdraws from the vault. // All preceding transactions fund that vault and could run in parallel, // but we send them sequentially here for clarity and to ensure order. const commitment = sdk.state.getCommitment(); console.log("\n🔑 Signing and sending transactions sequentially (you can parallelize pre-vault txs if desired)..."); for (let i = 0; i < claimTransactions.length; i++) { const { transaction, blockhash } = claimTransactions[i]; const isFinal = i === claimTransactions.length - 1; const label = isFinal ? "vault withdraw" : "pre-vault"; console.log(`\n📝 Processing transaction ${i + 1}/${claimTransactions.length} (${label})...`); const signature = await signAndSendTransaction(connection, commitment, transaction, keypair, blockhash); console.log(`✅ ${label} transaction sent: ${signature}`); } console.log("\n🎉 Partner fee claiming completed successfully!"); // Get updated stats const updatedStats = await sdk.partner.getPartnerConfigClaimStats(partnerWallet); const newUnclaimedFees = BigInt(updatedStats.unclaimedFees); const newUnclaimedFeesSOL = Number(newUnclaimedFees) / 1_000_000_000; console.log(`\n📊 Updated Stats:`); console.log(` Unclaimed Fees: ${newUnclaimedFeesSOL.toFixed(9)} SOL`); } catch (error) { console.error("🚨 Partner fee claiming failed:", error); throw error; } } // Example: Claim partner fees for a specific wallet // Replace with your partner wallet address const partnerWallet = new PublicKey("YOUR_PARTNER_WALLET_ADDRESS_HERE"); claimPartnerFees(partnerWallet) .then(() => { console.log("\n✨ Process completed!"); }) .catch((error) => { console.error("🚨 Unexpected error occurred:", error); }); ``` ### 3. Understanding Partner Fees Partner fees are accumulated when token launches include your partner configuration. Here's how it works: ### Partner Fee Accumulation * **Token Launches**: When tokens are launched with your partner config, fees from trading are automatically accumulated in your partner configuration * **Fee Share**: The percentage you receive is determined by the partner configuration's BPS (basis points) setting * **Automatic Tracking**: The SDK tracks all accumulated fees across all token launches that include your partner config ### Claiming Process 1. **Check Stats**: Use `getPartnerConfigClaimStats()` to see how much you can claim 2. **Get Transactions**: Use `getPartnerConfigClaimTransactions()` to generate claim transactions 3. **Sign & Send**: Sign and broadcast the transactions to claim your fees ### Fee Types * **Claimed Fees**: Fees you've already claimed and received * **Unclaimed Fees**: Fees that are available to claim but haven't been claimed yet ### 4. Running the Script To check and claim partner fees, edit the `partnerWallet` variable in `claim-partner-fees.ts` with your partner wallet address. Then, run the script from your terminal: ```bash theme={null} npx ts-node claim-partner-fees.ts ``` The script will: 1. Check if your partner config exists 2. Display your partner configuration details 3. Show your claimable fee stats 4. Generate and send claim transactions if fees are available 5. Display updated stats after claiming ### Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. Claim partner fees from the terminal without writing scripts: ```bash theme={null} bags partner claim ``` Add `--skip-confirm` to bypass the confirmation prompt: ```bash theme={null} bags partner claim --skip-confirm ``` To check your partner stats before claiming: ```bash theme={null} bags partner stats --partner YOUR_PARTNER_WALLET_PUBKEY ``` ### 5. Checking Stats Only If you just want to check your partner fee stats without claiming, you can use this simplified version: ```typescript theme={null} async function checkPartnerStats(partnerWallet: PublicKey) { const stats = await sdk.partner.getPartnerConfigClaimStats(partnerWallet); const claimedFees = BigInt(stats.claimedFees); const unclaimedFees = BigInt(stats.unclaimedFees); const claimedFeesSOL = Number(claimedFees) / 1_000_000_000; const unclaimedFeesSOL = Number(unclaimedFees) / 1_000_000_000; console.log(`💰 Claimed Fees: ${claimedFeesSOL.toFixed(9)} SOL`); console.log(`💰 Unclaimed Fees: ${unclaimedFeesSOL.toFixed(9)} SOL`); } ``` ### 6. Troubleshooting Common issues include: * **Partner Config Not Found**: Ensure you've created a partner key first using the [Create Partner Key](/how-to-guides/create-partner-key) guide * **No Unclaimed Fees**: If unclaimed fees are 0, there are no fees available to claim at this time * **Insufficient SOL**: Your wallet needs SOL for transaction fees * **Invalid Wallet Address**: Ensure the partner wallet address is a valid Solana public key * **Transaction Failures**: Check that your wallet has sufficient SOL for transaction fees ### 7. Related Guides * [Create Partner Key](/how-to-guides/create-partner-key) - Learn how to create a partner configuration * [Launch a Token](/how-to-guides/launch-token) - Learn how to include partner configs in token launches * [Claim Token Fees](/how-to-guides/claim-fees) - Learn how to claim token fees For more details, see the [API Reference](/api-reference/introduction). # Create a Launch Intent URL Source: https://docs.bags.fm/how-to-guides/create-launch-intent Build shareable launch intent URLs that pre-fill the Bags /launch form and embed them as buttons on any webpage A **launch intent** is a shareable URL that pre-fills the `/launch` form on Bags. You build the URL once (with a token name, ticker, description, image, fee sharing, partner overrides, and more), share it anywhere, and the recipient lands on the launch form with everything filled in. They review and click launch. In this guide you'll learn how to build a launch intent URL from scratch, how to embed it as a button on your own webpage, and how to attach your partner key so every launch routed through your link earns you a share of the trading fees. ## How it works When a user opens a launch intent URL, the Bags `/launch` page: 1. Parses every supported query param. 2. Hydrates the form fields, toggles (fee sharing, tokenize equity), the admin wallet, partner overrides, and optionally fetches the image URL into the upload slot. 3. Replaces the URL with `/launch` so the share link stays reusable and the user sees a clean URL. 4. Shows a toast confirming the prefill (plus a separate warning toast if any individual field failed validation). The trigger param `intent=true` is the **only required** value. Any other param is optional, and the page ignores all params if `intent=true` is missing. ``` https://bags.fm/launch?intent=true&name=MyCoin&ticker=MC&description=... ``` ## Prerequisites No API key is required to build a launch intent URL — it is purely client-side. **Optional — earn fees from launches via your link.** If you create a partner key and include it as the `partner` and `partnerConfig` params, every launch routed through your intent URL will credit the partner share to your key. See [Create a Partner Key](/how-to-guides/create-partner-key) before continuing if you want this behaviour. ## Encoding rules * Each param value must be passed through `encodeURIComponent`. Using `URLSearchParams` or `URL.searchParams.set` handles this for you automatically. * For complex values (arrays and objects), serialize them with `JSON.stringify` first, then set them on the search params. * All basis-point values use the convention `2500 = 25%`, `10000 = 100%`. The sum of `allocationBps` across all `feeShare` entries must be `<= 10000`. Minimal pattern: ```ts theme={null} const url = new URL("https://bags.fm/launch"); url.searchParams.set("intent", "true"); url.searchParams.set("name", "My Coin"); url.searchParams.set( "feeShare", JSON.stringify([ { allocationBps: 5000, platform: "twitter", username: "bagsapp" }, ]) ); const shareUrl = url.toString(); ``` ## Supported parameters ### Basic text fields * `name` — token name, max 32 characters. * `ticker` — alphanumeric, max 10 characters. Auto-uppercased by the form. * `description` — free-form token description. * `website` — any valid URL. Automatically expands the "Social links" section on the form. * `twitter` — `https://twitter.com/...` or `https://x.com/...` URL. Also auto-expands social links. ### Image * `image` — absolute `http://` or `https://` URL to an image. * The page fetches the URL client-side, so the host must allow CORS. If CORS fails the user sees a "Could not load image from link" toast and must upload the image manually. * The response `Content-Type` header is used as the file mime type. A non-image mime is rejected. * The filename is derived from the URL pathname, falling back to `intent-image.`. ### Initial buy * `initialBuy` — numeric string representing a USD amount, for example `"100"`. Negative or non-finite values are silently ignored. ### Fee mode * `feeMode` — one of the following `METEORA_CONFIG_TYPE` keys: * `DEFAULT` — Founder Mode, 1% of trading volume. * `BPS100PRE_BPS25POST_5000_COMPOUNDING` — \~0% mode. * `BPS1000PRE_BPS1000POST` — Paper Hand Tax mode. * `BPS25PRE_BPS100POST_5000_COMPOUNDING` — advanced. * `BPS1000PRE_BPS1000POST_5000_COMPOUNDING` — advanced. * Unknown keys are ignored and produce a warning toast. Some fee modes disable fee sharing in the UI (for example `BPS100PRE_BPS25POST_5000_COMPOUNDING`). If you set both `feeMode` and fee-sharing params, the mode wins: `feeShareEnabled`, `feeShareType`, and `feeShare` are dropped at parse time and a warning toast is shown. ### Fee sharing * `feeShareEnabled` — `"true"` toggles the fee-sharing section on. Also auto-enabled when a non-empty `feeShare` array is provided. * `feeShareType` — `"multi"` (default) or `"csv"`. Controls which tab is shown. * `feeShare` — JSON array. Each entry has this shape: ```json theme={null} { "allocationBps": 2500, "platform": "twitter", "username": "bagsapp" } ``` Rules: * `allocationBps`: positive integer, basis points (`2500 = 25%`). The sum across all entries must be `<= 10000`. The floor of each value is applied. * `platform`: one of the enabled platforms: `twitter`, `tiktok`, `github`, `moltbook`, `solana`, `kick`. Disabled platforms (`instagram`, `twitch`, `onlyfans`) are rejected. * `username`: non-empty string. When `platform` is `"solana"`, this must be a base58 Solana public key. * At most 100 entries. Extras are truncated with a warning. ### Admin wallet * `admin` — base58 Solana public key. Sets the admin wallet when fee sharing is enabled. * Invalid pubkeys are ignored with a warning. * The admin field is only editable in the UI when fee sharing is on, but the intent still applies the value. ### Tokenize equity * `tokenizeEquity` — `"true"` toggles the tokenize-equity section on. Auto-enabled if `equity` is provided. * `equity` — JSON object. Full shape: ```json theme={null} { "projectName": "Acme Corp", "bedrockShareBasisPoint": 2500, "category": "DEFI", "twitterHandle": "acme", "preferredCompanyNames": ["Acme Inc", "Acme Labs Inc", "Acme Holdings Inc"], "founders": [ { "firstName": "Alice", "lastName": "Anderson", "email": "alice@acme.co", "nationalityCountry": "USA", "taxResidencyCountry": "USA", "residentialAddress": "123 Main St, NY", "shareBasisPoint": 7500 } ] } ``` Field notes: * `projectName`: string, required by the form at submit time. * `bedrockShareBasisPoint`: integer bps. Defaults to `2500` (25%) if missing. The sum of all `founders[].shareBasisPoint` plus `bedrockShareBasisPoint` must equal exactly `10000` at submit time. * `category`: one of `RWA`, `AI`, `DEFI`, `INFRA`, `DEPIN`, `LEGAL`, `GAMING`, `NFT`, `MEME`, or the empty string `""`. Unknown values are dropped with a warning. * `twitterHandle`: 1–15 alphanumeric/underscore characters, no `@` prefix. Validated at submit time. * `preferredCompanyNames`: array of exactly 3 unique strings (the form enforces uniqueness at submit time). * `founders`: array of founder objects. All string fields default to empty, `shareBasisPoint` defaults to `0`. ### Partner override These two params are not editable in the UI. They override the ref-code partner lookup that would otherwise come from the browser cookie. * `partner` — base58 Solana public key. * `partnerConfig` — base58 Solana public key. Both values are passed directly to the launch transaction at submit time. Invalid pubkeys are ignored with a warning. **These keys are a matched on-chain pair.** If you supply one, you must supply the other — the launch uses the override pair exclusively and will not fall back to the ref-code pair for the missing half. Always emit both together, or omit both. See [Create a Partner Key](/how-to-guides/create-partner-key) for how to obtain these values. ### UI state * `showSocial` — `"true"` pre-expands the "Social links (optional)" collapsible. Automatically `true` when `website` or `twitter` is present. ### Boolean values Any param documented as boolean accepts: * `"true"` or `"1"` → true * `"false"` or `"0"` → false * missing → undefined (no change to the default) ## Error handling The launch page never fails the whole intent for a single bad field. Instead, it ignores the offending value and surfaces a warning: * Invalid JSON for `feeShare` or `equity` → the param is ignored, the rest of the intent still applies, warning toast. * Invalid Solana pubkey (`admin`, `partner`, `partnerConfig`) → ignored, warning toast. * Unknown `feeMode` key → ignored, warning toast. * Image fetch failure (CORS, 404, non-image mime) → error toast, user must upload manually. If any warnings occur, a consolidated "Some intent fields were ignored" toast is shown with a `(+N more)` counter, and every warning is also logged to `console.warn`. Since nothing is validated server-side, your builder UI should enforce the same rules before producing the link. ## Build an intent URL in TypeScript Here is a reusable helper that serializes a typed input into a launch intent URL. Drop it into your builder UI or a server that returns share links: ```ts theme={null} type FeeSharePlatform = | "twitter" | "tiktok" | "github" | "moltbook" | "solana" | "kick"; interface FeeShareEntry { allocationBps: number; platform: FeeSharePlatform; username: string; } interface BedrockFounder { firstName: string; lastName: string; email: string; nationalityCountry: string; taxResidencyCountry: string; residentialAddress: string; shareBasisPoint: number; } interface EquityIntent { projectName: string; bedrockShareBasisPoint?: number; category?: | "RWA" | "AI" | "DEFI" | "INFRA" | "DEPIN" | "LEGAL" | "GAMING" | "NFT" | "MEME" | ""; twitterHandle?: string; preferredCompanyNames?: [string, string, string]; founders?: BedrockFounder[]; } interface LaunchIntent { name?: string; ticker?: string; description?: string; website?: string; twitter?: string; image?: string; initialBuy?: string | number; feeMode?: | "DEFAULT" | "BPS100PRE_BPS25POST_5000_COMPOUNDING" | "BPS1000PRE_BPS1000POST" | "BPS25PRE_BPS100POST_5000_COMPOUNDING" | "BPS1000PRE_BPS1000POST_5000_COMPOUNDING"; feeShareEnabled?: boolean; feeShareType?: "multi" | "csv"; feeShare?: FeeShareEntry[]; admin?: string; tokenizeEquity?: boolean; equity?: EquityIntent; partner?: string; partnerConfig?: string; showSocial?: boolean; } export function buildLaunchIntentUrl( intent: LaunchIntent, origin = "https://bags.fm" ): string { const url = new URL("/launch", origin); const params = url.searchParams; params.set("intent", "true"); const setIf = (key: string, value: string | number | undefined) => { if (value === undefined || value === null || value === "") return; params.set(key, String(value)); }; const setBool = (key: string, value: boolean | undefined) => { if (value === undefined) return; params.set(key, value ? "true" : "false"); }; const setJson = (key: string, value: unknown) => { if (value === undefined) return; params.set(key, JSON.stringify(value)); }; setIf("name", intent.name); setIf("ticker", intent.ticker); setIf("description", intent.description); setIf("website", intent.website); setIf("twitter", intent.twitter); setIf("image", intent.image); setIf("initialBuy", intent.initialBuy); setIf("feeMode", intent.feeMode); setBool("feeShareEnabled", intent.feeShareEnabled); setIf("feeShareType", intent.feeShareType); if (intent.feeShare && intent.feeShare.length > 0) { setJson("feeShare", intent.feeShare); } setIf("admin", intent.admin); setBool("tokenizeEquity", intent.tokenizeEquity); if (intent.equity) { setJson("equity", intent.equity); } if (intent.partner && intent.partnerConfig) { params.set("partner", intent.partner); params.set("partnerConfig", intent.partnerConfig); } setBool("showSocial", intent.showSocial); return url.toString(); } ``` Use it: ```ts theme={null} const shareUrl = buildLaunchIntentUrl({ name: "BagsCoin", ticker: "BAGS", description: "The launchpad token.", website: "https://bags.fm", twitter: "https://twitter.com/bagsapp", image: "https://cdn.example.com/bags-logo.png", initialBuy: 250, feeMode: "DEFAULT", feeShareEnabled: true, feeShareType: "multi", feeShare: [ { allocationBps: 5000, platform: "twitter", username: "bagsapp" }, { allocationBps: 2500, platform: "solana", username: "So11111111111111111111111111111111111111112", }, ], admin: "BAGSB9TpGrZxQbEsrEznv5jXXdwyP6AXerN8aVRiAmcv", partner: "YourPartnerWalletPubkeyHere", partnerConfig: "YourPartnerConfigPdaHere", }); console.log(shareUrl); ``` Always emit `partner` and `partnerConfig` together. The helper above enforces this by only setting them when both are present. ## Embed a launch button on your webpage Once you have a share URL, dropping a "Launch on Bags" button onto any webpage is straightforward. Pick whichever flavour matches your stack. ### Plain HTML If your intent is static (for example, a marketing page for a single token you want to promote), paste the URL into an anchor styled as a button: ```html theme={null} Launch on Bags ``` ### Vanilla JavaScript If the intent depends on page state (for example, a form the user is filling in), build the URL at click time: ```html theme={null} ``` ### React component If you're already on React, wrap the builder in a component: ```tsx theme={null} import { buildLaunchIntentUrl, type LaunchIntent } from "./launch-intent"; interface LaunchIntentButtonProps { intent: LaunchIntent; label?: string; className?: string; } export function LaunchIntentButton({ intent, label = "Launch on Bags", className, }: LaunchIntentButtonProps) { const href = buildLaunchIntentUrl(intent); return ( {label} ); } ``` Usage: ```tsx theme={null} ``` The `image` param must be a **public URL**. Local `File` uploads from a file input cannot be encoded into a URL — if you need a custom image, upload it to your CDN first and pass the resulting URL. ## Earn fees from launches via your link Attaching your partner key turns every share into a revenue stream: when a user completes a launch through your intent URL, the partner share of trading fees is routed to your key instead of to the ref-code partner that would otherwise be looked up from the cookie. To enable this: 1. Create a partner key by following [Create a Partner Key](/how-to-guides/create-partner-key). You end up with: * a **partner wallet pubkey** (your wallet) → becomes the `partner` param * a **partner config PDA** → becomes the `partnerConfig` param 2. Pass both values to `buildLaunchIntentUrl` (or whichever builder you use). 3. Periodically claim the accumulated fees — see [Claim Partner Fees](/how-to-guides/claim-partner-fees). The `partner` and `partnerConfig` values are a matched on-chain pair. If you set one without the other, the launch page ignores both and shows a warning toast. Always include the pair together. ## Notes and caveats * **No server-side validation.** The launch page parses params on the client only. Your builder UI should enforce the same rules (bps sum ≤ 10000, valid Solana pubkeys, image URL reachable, exactly 3 unique `preferredCompanyNames`, etc.) before producing the link so the recipient never sees warning toasts. * **The URL is cleaned after hydration.** Once the form is populated, the recipient's browser replaces the URL with `/launch`. Do not rely on hashes or anchors persisting. * **Image files cannot be encoded.** A `File` object from an `` cannot be put into a URL. Upload the image to a CDN first and pass the resulting public URL as the `image` param. * **Keep links short.** Only emit params that differ from the form defaults. For example, skip `feeMode=DEFAULT` and skip `showSocial` when `website` or `twitter` is already present (both auto-expand the social links section). * **Keep your partner pair together.** Emitting `partner` without `partnerConfig` (or vice versa) drops both values at parse time. # Create Partner Key Source: https://docs.bags.fm/how-to-guides/create-partner-key Complete guide to create a partner key for fee sharing using the Bags Dev Dashboard or SDK In this guide, you'll learn how to create a partner key (partner config key) for fee sharing. Partner keys allow you to receive a share of fees from token launches that include your partner configuration. You can create a partner key using either the Bags Dev Dashboard or the TypeScript SDK. **Transaction Fees**: Creating a partner key requires a Solana transaction. Make sure your wallet has sufficient SOL balance to pay for transaction fees. **One key per wallet**: Each wallet can have only one partner key. If you need multiple partner keys, use multiple wallets and create a partner key for each wallet via the SDK. **Default fee share**: By default, a partner key receives 25% (2,500 bps) of the fees generated by tokens launched via that partner key. If you need a custom percentage, reach out to us and we can configure it for your account. ## Method 1: Using the Dev Dashboard The easiest way to create a partner key is through the Bags Developer Dashboard. ### Step 1: Access the Dashboard 1. Go to [https://dev.bags.fm](https://dev.bags.fm) and log in with your account. ### Step 2: Create Partner Key 1. Click the **"Create partner key"** button in the dashboard. 2. Confirm creation in the confirmation modal by clicking the **"Create partner key"** button 3. Note: Only one partner key can be created per wallet. Create Partner Key Button Partner Key Table ### Step 3: View Your Partner Config 1. After creating your partner key, you can view it in the Partner Key table below. 2. You can copy your partner config key (PDA) from the table. 3. The table also displays your claim stats, including claimed and unclaimed fees. Your partner config key is now ready to use in token launches! See the [Launch a Token](/how-to-guides/launch-token) guide for details on how to include it. ## Method 2: Using the SDK You can also create a partner key programmatically using the Bags TypeScript SDK. ### Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with some SOL for transaction fees. * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` ### 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ### 2. The Partner Key Creation Script Here is the complete script for creating a partner key. Save it as `create-partner-key.ts`. ### Endpoints Used Under the Hood This SDK flow uses: * [`POST /fee-share/partner-config/creation-tx`](/api-reference/create-partner-configuration) via `sdk.partner.getPartnerConfigCreationTransaction()` `sdk.partner.getPartnerConfig()` is an on-chain account read (PDA fetch), not a Bags HTTP API endpoint. ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, deriveBagsFeeShareV2PartnerConfigPda, signAndSendTransaction } from "@bagsfm/bags-sdk"; import { Keypair, PublicKey, Connection, } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function createPartnerKey(partnerWallet: PublicKey) { try { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); console.log( `🔑 Creating partner key for wallet: ${partnerWallet.toBase58()}` ); // Check if partner config already exists const partnerConfigPda = deriveBagsFeeShareV2PartnerConfigPda(partnerWallet); console.log(`📍 Partner Config PDA: ${partnerConfigPda.toBase58()}`); try { const existingConfig = await sdk.partner.getPartnerConfig(partnerWallet); console.log("♻️ Partner config already exists!"); console.log(` Partner: ${existingConfig.partner.toBase58()}`); console.log(` BPS: ${existingConfig.bps}`); console.log( ` Total Claimed Fees: ${existingConfig.totalClaimedFees.toString()}` ); return partnerConfigPda; } catch (error: any) { if (!error.message?.includes("not found")) { throw error; } // Partner config doesn't exist, proceed with creation } // Get the partner config creation transaction console.log("📝 Getting partner config creation transaction..."); const { transaction, blockhash } = await sdk.partner.getPartnerConfigCreationTransaction(partnerWallet); // Sign and send the transaction const commitment = sdk.state.getCommitment(); console.log("🔐 Signing and sending transaction..."); const signature = await signAndSendTransaction(connection, commitment, transaction, keypair, blockhash); console.log("🎉 Partner key created successfully!"); console.log(`📍 Partner Config PDA: ${partnerConfigPda.toBase58()}`); console.log(`🔑 Transaction Signature: ${signature}`); return partnerConfigPda; } catch (error) { console.error("🚨 Partner key creation failed:", error); throw error; } } // Example: Create a partner key for a specific wallet // Replace with your partner wallet address const partnerWallet = new PublicKey("YOUR_PARTNER_WALLET_ADDRESS_HERE"); createPartnerKey(partnerWallet) .then((partnerConfigPda) => { console.log( `\n✨ Partner key ready! Use this Partner Config PDA in your token launches:` ); console.log(` ${partnerConfigPda.toBase58()}`); }) .catch((error) => { console.error("🚨 Unexpected error occurred:", error); }); ``` ### 3. Understanding Partner Keys A partner key (partner config) is a program-derived address (PDA) that represents a partner's configuration for receiving fee shares. When you create a partner key: 1. **Partner Wallet**: The wallet address that will receive the partner's share of fees 2. **Partner Config PDA**: A derived address that uniquely identifies the partner configuration 3. **Fee Share**: By default, partner keys receive 25% (2,500 bps) of fees from tokens launched via their key. Custom percentages are available on request—contact us to discuss. 4. **Per-Wallet Limit**: Each wallet can have only one partner key. To use multiple partner keys, manage multiple wallets and create a partner key for each using the SDK. ### When to Use Partner Keys Partner keys are useful when: * You want to receive a share of fees from multiple token launches * You're building a platform that launches tokens and wants to collect fees * You have a partnership agreement to receive fees from specific token launches ### Using Partner Keys in Token Launches Once you have a partner config PDA, you can use it when creating fee share configurations for token launches. See the [Launch a Token](/how-to-guides/launch-token) guide for details on how to include partner and partnerConfig parameters. ### 4. Run Your Script To create a partner key, edit the `partnerWallet` variable in `create-partner-key.ts` with the wallet address that should receive the partner fees. Then, run the script from your terminal: ```bash theme={null} npx ts-node create-partner-key.ts ``` ### Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. Create a partner key directly from the terminal: ```bash theme={null} bags partner create ``` Add `--skip-confirm` to bypass the confirmation prompt: ```bash theme={null} bags partner create --skip-confirm ``` After creation, check your partner stats: ```bash theme={null} bags partner stats --partner YOUR_WALLET_PUBKEY ``` ### 5. Next Steps After creating a partner key, you can: * **Check Partner Stats**: Use `sdk.partner.getPartnerConfigClaimStats()` to see accumulated fees * **Claim Partner Fees**: See the [Claim Partner Fees](/how-to-guides/claim-partner-fees) guide for a complete walkthrough on checking and claiming your partner fees * **Use in Token Launches**: Include your partner config when launching tokens (see [Launch a Token](/how-to-guides/launch-token) guide) ### 6. Troubleshooting Common issues include: * **Partner Config Already Exists**: If the partner config already exists, the script will detect it and display the existing configuration. * **Insufficient SOL**: Your wallet needs SOL for transaction fees. * **Invalid Wallet Address**: Ensure the partner wallet address is a valid Solana public key. For more details, see the [API Reference](/api-reference/introduction). # Customize Token Fees Source: https://docs.bags.fm/how-to-guides/customize-token-fees Learn how to configure different fee structures for your token launch using Bags config types When launching a token via the Bags API, you can customize the fee structure by passing the optional `bagsConfigType` parameter to the [Create Fee Share Config](/api-reference/create-fee-share-configuration) endpoint. This controls how much in trading fees the token takes at different stages of its lifecycle. ## Fee Distribution All trading fees are split between the **protocol** and the **creator**. The split depends on whether fee compounding is enabled: | Setup | Protocol | Creator | Compounding | | --------------- | ------------ | ------------ | ----------- | | No compounding | 50% of fee | 50% of fee | — | | 25% compounding | 37.5% of fee | 37.5% of fee | 25% of fee | | 50% compounding | 25% of fee | 25% of fee | 50% of fee | For example, a 2% fee without compounding gives 1% to the protocol and 1% to the creator. A 2% fee with 25% compounding gives 0.5% to compounding, 0.75% to the protocol, and 0.75% to the creator. A 1% fee with 50% compounding gives 0.5% to compounding, 0.25% to the protocol, and 0.25% to the creator. ## Fee Modes There are seven available fee modes. If you don't specify a `bagsConfigType`, the **Default** mode is used. ### Default **Config ID:** `fa29606e-5e48-4c37-827f-4b03d58ee23d` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 2% | 1% | 1% | — | | Post-migration | 2% | 0.75% | 0.75% | 0.5% | The standard fee structure. A flat 2% fee on all trades. Pre-migration, the fee is split equally between protocol and creator. Post-migration, 25% of fees are compounded back into the pool's liquidity, with the remainder split equally between protocol and creator. This is the simplest option and works well for most token launches. ### Low Pre / High Post with Compounding **Config ID:** `d16d3585-6488-4a6c-9a6f-e6c39ca0fda3` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 0.25% | 0.125% | 0.125% | — | | Post-migration | 1% | 0.25% | 0.25% | 0.5% | Lower fees during the bonding curve phase to encourage early trading volume, then a higher fee rate once the token graduates to the DAMM V2 pool. Post-migration, 50% of fees are compounded back into the pool's liquidity, deepening the order book over time. ### High Pre / Low Post with Compounding **Config ID:** `a7c8e1f2-3d4b-5a6c-9e0f-1b2c3d4e5f6a` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 1% | 0.5% | 0.5% | — | | Post-migration | 0.25% | 0.0625% | 0.0625% | 0.125% | Higher fees during the bonding curve phase to maximize early fee revenue, then reduced fees post-migration to encourage continued trading. Post-migration, 50% of fees are compounded back into the pool's liquidity. ### High Flat with Compounding **Config ID:** `48e26d2f-0a9d-4625-a3cc-c3987d874b9e` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 10% | 5% | 5% | — | | Post-migration | 10% | 2.5% | 2.5% | 5% | A high flat 10% fee on all trades. Post-migration, 50% of fees are compounded back into the pool's liquidity, rapidly deepening the order book. Best suited for tokens that want to maximize fee revenue and liquidity growth simultaneously. ### 2% Flat with 85% Supply Locked **Config ID:** `810faadb-030b-47de-a68b-7211c1cbbee3` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 2% | 1% | 1% | — | | Post-migration | 2% | 0.75% | 0.75% | 0.5% | Identical fee economics to the **Default** mode (a flat 2% fee, with 25% of post-migration fees compounded back into the pool's liquidity), but **85% of the token supply is locked**. Use this when you want the standard 2% fee structure while keeping the large majority of supply locked. This mode graduates at **\~100 SOL** rather than the usual 85 SOL. ### Default with 1K Supply **Config ID:** `e2963a6c-441a-4862-9d80-b94e3a481cd7` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | --------- | -------- | ------- | ----------- | | Pre-migration | 2% | 1% | 1% | — | | Post-migration | 2% | 0.75% | 0.75% | 0.5% | Identical to the **Default** mode in every way (a flat 2% fee with 25% post-migration compounding), except the token launches with a **1,000 token supply**. Use this when you specifically need a 1K total supply. ### 2% Base with 96% Supply Locked **Config ID:** `ba28db46-ea6f-4452-8218-5587f6aca0a1` | Stage | Total Fee | Protocol | Creator | Compounding | | -------------- | ----------------------------------- | ------------ | ------------ | ----------- | | Pre-migration | 2% | 1% | 1% | — | | Post-migration | 2% → 0.5% by market cap (see below) | 37.5% of fee | 37.5% of fee | 25% of fee | Like the **Default** mode, 25% of post-migration fees are compounded back into the pool's liquidity and the remainder is split equally between protocol and creator. This mode adds two things on top: **96% of the token supply is locked**, and a **market-cap-based fee post-migration** (starting at 2% and decaying to a 0.5% floor). **Post-migration market-cap fee:** the 2% base fee only holds during the bonding-curve phase. Once the token graduates (raises 55 SOL and migrates to the real DAMM v2 pool), the trading fee no longer stays flat — it starts at 2% and shrinks as the token's market cap grows, down to a **0.5% floor**. The bigger and more successful the token gets, the cheaper it is to trade. The decay is exponential (it drops faster early, then eases toward the floor) and bottoms out once the market cap is roughly **25x** its value at graduation: ```text theme={null} 1x MC (just graduated): 2.00% ~2x : ~1.45% ~5x : ~1.00% ~9x : ~0.80% ~15x : ~0.63% ~25x and beyond : 0.50% <- floor, stays here ``` In absolute terms for this mode (graduation ≈ 4,978 SOL FDV): \~2% at the start of the DAMM v2 pool, \~1% around \~25k SOL FDV, and the 0.5% floor around \~125k SOL FDV. As with the Default mode, 25% of each post-migration fee is compounded back into the pool's liquidity and the remaining 75% is split equally between protocol and creator. ## `enableFirstSwapWithMinFee` When creating a fee share config, you can pass an optional boolean `enableFirstSwapWithMinFee` (default `true`) to the [Create Fee Share Config](/api-reference/create-fee-share-configuration) endpoint. When `true`, the **first** swap on the new pool pays the minimum (ending) base fee instead of the starting cliff fee. This only matters for config types that use a **base-fee scheduler** (one whose base fee changes over time). **No current config type uses a base-fee scheduler, so this field has no effect right now** — you can safely omit it. It is retained for forward compatibility in case scheduler-based config types are introduced later. ## Choosing the Right Mode | Goal | Recommended Mode | | ------------------------------------------------------------ | ------------------------------------------ | | Simple, consistent fees | **Default** (2% / 2%) | | Encourage early trading, earn more post-graduation | **Low Pre / High Post** (0.25% / 1%) | | Maximize early fee revenue, encourage post-graduation volume | **High Pre / Low Post** (1% / 0.25%) | | Maximize fee revenue and liquidity growth | **High Flat with Compounding** (10% / 10%) | | Standard 2% fees with most supply locked | **2% Flat with 85% Supply Locked** | | Default fees, but a 1,000 token supply | **Default with 1K Supply** | | Lock nearly all supply; fees that fall as the token grows | **2% Base with 96% Supply Locked** | The `bagsConfigType` is set once when creating the fee share config and cannot be changed after the token is launched. Choose your fee structure carefully before launching. ## Graduation Thresholds Every token launches on a bonding curve and **graduates** (migrates to a DAMM V2 pool) once it raises a set amount of SOL. Most modes graduate at **85 SOL**; the supply-locked modes differ. | Mode | SOL to graduate | | ------------------------------------ | --------------- | | Default | 85 SOL | | Low Pre / High Post with Compounding | 85 SOL | | High Pre / Low Post with Compounding | 85 SOL | | High Flat with Compounding | 85 SOL | | 2% Flat with 85% Supply Locked | \~100 SOL | | Default with 1K Supply | 85 SOL | | 2% Base with 96% Supply Locked | 55 SOL | ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. You can create and manage fee share configurations directly from the terminal: **Create a fee share config:** ```bash theme={null} bags config create \ --mint TOKEN_MINT \ --fee-claimers '[{"user":"WALLET_A","userBps":5000},{"user":"WALLET_B","userBps":5000}]' ``` **Update an existing config (requires admin authority):** ```bash theme={null} bags config update \ --mint TOKEN_MINT \ --fee-claimers '[{"user":"WALLET_A","userBps":7000},{"user":"WALLET_B","userBps":3000}]' ``` **Transfer admin authority to another wallet:** ```bash theme={null} bags config transfer-admin --mint TOKEN_MINT --new-admin NEW_ADMIN_PUBKEY ``` **List all tokens where you are admin:** ```bash theme={null} bags config admin-list ``` Add `--skip-confirm` to any command to bypass the confirmation prompt. ## What is Fee Compounding? All seven fee modes enable **fee compounding** after migration. This means that a portion of the trading fees collected post-migration are automatically reinvested into the DAMM V2 pool's liquidity rather than being distributed. This deepens the pool over time, leading to tighter spreads and better trading conditions for the token. The **Default** mode and the three supply-locked/supply variants (**2% Flat with 85% Supply Locked**, **Default with 1K Supply**, and **2% Base with 96% Supply Locked**) compound **25%** of post-migration fees, while the other three modes (**Low Pre / High Post**, **High Pre / Low Post**, and **High Flat with Compounding**) compound **50%**. The remaining fees are split equally between the protocol and the creator. # Get Token Claim Events Source: https://docs.bags.fm/how-to-guides/get-token-claim-events Learn how to retrieve claim events for a token using both offset pagination and time-based filtering In this guide, you'll learn how to retrieve claim events for a Solana token using the Bags TypeScript SDK. The endpoint supports two query modes: **offset-based pagination** for traditional page-by-page retrieval, and **time-based filtering** for fetching events within a specific time range. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * The token mint address you want to analyze. ## Endpoint Used Under the Hood This guide uses: * [`GET /fee-share/token/claim-events`](/api-reference/get-token-claim-events) via `sdk.state.getTokenClaimEvents()` ## Query Modes Overview | Mode | Use Case | Required Parameters | | -------- | -------------------------------- | ------------------------------ | | `offset` | Paginated lists, real-time feeds | `limit`, `offset` | | `time` | Historical analysis, reports | `from`, `to` (unix timestamps) | ## 1. Offset Mode (Pagination) Use offset mode to paginate through claim events. This is the default mode and is backward compatible with previous API versions. ### Script: Paginated Claim Events Save this as `get-claim-events-paginated.ts`: ```typescript get-claim-events-paginated.ts theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK } from "@bagsfm/bags-sdk"; import { PublicKey, Connection } from "@solana/web3.js"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; if (!BAGS_API_KEY || !SOLANA_RPC_URL) { throw new Error("BAGS_API_KEY and SOLANA_RPC_URL are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function getClaimEventsWithOffset(tokenMint: string, limit = 100, offset = 0) { console.log(`🔍 Fetching claim events for: ${tokenMint}`); console.log(` Mode: offset | Limit: ${limit} | Offset: ${offset}\n`); const events = await sdk.state.getTokenClaimEvents(new PublicKey(tokenMint), { mode: "offset", limit, offset, }); events.forEach((event, i) => { console.log(`Event ${i + 1}:`); console.log(` Wallet: ${event.wallet}`); console.log(` Amount: ${event.amount} lamports`); console.log(` Creator: ${event.isCreator ? "Yes" : "No"}`); console.log(` Time: ${new Date(event.timestamp).toLocaleString()}`); console.log(` Signature: ${event.signature.slice(0, 20)}...`); console.log(); }); console.log(`✅ Retrieved ${events.length} claim events`); return events; } // Example: Fetch first 10 events getClaimEventsWithOffset("CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS", 10, 0) .catch(console.error); ``` ### Run the Script ```bash npx theme={null} npx ts-node get-claim-events-paginated.ts ``` ```bash bun theme={null} bun get-claim-events-paginated.ts ``` ## 2. Time Mode (Time-Based Filtering) Use time mode to retrieve all claim events within a specific time range. This is useful for analytics, generating reports, or syncing historical data. ### Script: Time-Based Claim Events Save this as `get-claim-events-by-time.ts`: ```typescript get-claim-events-by-time.ts theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK } from "@bagsfm/bags-sdk"; import { LAMPORTS_PER_SOL, PublicKey, Connection } from "@solana/web3.js"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; if (!BAGS_API_KEY || !SOLANA_RPC_URL) { throw new Error("BAGS_API_KEY and SOLANA_RPC_URL are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); // Helper to convert date to unix timestamp function toUnixTimestamp(date: Date): number { return Math.floor(date.getTime() / 1000); } async function getClaimEventsByTimeRange(tokenMint: string, from: number, to: number) { console.log(`📅 Fetching claim events for: ${tokenMint}`); console.log(` Mode: time | From: ${from} | To: ${to}\n`); const events = await sdk.state.getTokenClaimEvents(new PublicKey(tokenMint), { mode: "time", from, to, }); // Calculate total claimed const totalLamports = events.reduce( (sum, e) => sum + BigInt(e.amount), BigInt(0) ); const totalSol = Number(totalLamports) / LAMPORTS_PER_SOL; console.log(`📊 Summary:`); console.log(` Total events: ${events.length}`); console.log(` Total claimed: ${totalSol.toLocaleString()} SOL`); console.log(` Unique wallets: ${new Set(events.map((e) => e.wallet)).size}`); // Show breakdown by day const byDay = new Map(); events.forEach((event) => { const day = new Date(event.timestamp).toLocaleDateString(); const existing = byDay.get(day) || { count: 0, amount: BigInt(0) }; byDay.set(day, { count: existing.count + 1, amount: existing.amount + BigInt(event.amount), }); }); console.log(`\n📆 Daily breakdown:`); byDay.forEach((stats, day) => { const sol = Number(stats.amount) / LAMPORTS_PER_SOL; console.log(` ${day}: ${stats.count} claims, ${sol.toLocaleString()} SOL`); }); return events; } // Example: Get events from the last 7 days async function main() { const tokenMint = "CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS"; const now = new Date(); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const from = toUnixTimestamp(sevenDaysAgo); const to = toUnixTimestamp(now); console.log(`🗓️ Date range: ${sevenDaysAgo.toLocaleDateString()} → ${now.toLocaleDateString()}\n`); await getClaimEventsByTimeRange(tokenMint, from, to); } main().catch(console.error); ``` ### Run the Script ```bash npx theme={null} npx ts-node get-claim-events-by-time.ts ``` ```bash bun theme={null} bun get-claim-events-by-time.ts ``` ## Example Output ### Offset Mode Output ``` 🔍 Fetching claim events for: CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS Mode: offset | Limit: 10 | Offset: 0 Event 1: Wallet: 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin Amount: 1500000000 lamports Creator: Yes Time: 1/15/2026, 3:45:00 PM Signature: 5KtPn1LGuxhKLHHD... Event 2: Wallet: 7nYXhxpZe9VuXMBZE3TVf... Amount: 500000000 lamports Creator: No Time: 1/14/2026, 2:30:00 PM Signature: 3RtQm2JGvxhMKIFD... ✅ Retrieved 10 claim events ``` ### Time Mode Output ``` 🗓️ Date range: 1/9/2026 → 1/16/2026 📅 Fetching claim events for: CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS Mode: time | From: 1736380800 | To: 1736985600 📊 Summary: Total events: 47 Total claimed: 125.5 SOL Unique wallets: 12 📆 Daily breakdown: 1/15/2026: 8 claims, 22.3 SOL 1/14/2026: 12 claims, 35.1 SOL 1/13/2026: 7 claims, 18.7 SOL 1/12/2026: 10 claims, 28.4 SOL 1/11/2026: 6 claims, 12.2 SOL 1/10/2026: 4 claims, 8.8 SOL ``` ## SDK Function Reference The `sdk.state.getTokenClaimEvents()` function accepts a token mint and an options object: ```typescript theme={null} // Offset mode (default) const events = await sdk.state.getTokenClaimEvents(tokenMint, { limit: 100, // 1-100, default: 100 offset: 0, // default: 0 }); // Time mode const events = await sdk.state.getTokenClaimEvents(tokenMint, { mode: "time", from: 1736380800, // unix timestamp (required) to: 1736985600, // unix timestamp (required, must be >= from) }); ``` ### Return Type Each event in the returned array has the following structure: | Field | Type | Description | | ----------- | --------- | --------------------------------------------------------- | | `wallet` | `string` | Public key of the wallet that claimed fees | | `isCreator` | `boolean` | Whether this wallet is the token creator | | `amount` | `string` | Amount claimed in lamports (as string for bigint support) | | `signature` | `string` | Transaction signature of the claim | | `timestamp` | `string` | ISO 8601 timestamp of the claim event | ## Use Cases ### Offset Mode * **Paginated UIs**: Display claim events in a table with "Load More" or page navigation * **Real-time Feeds**: Show the latest claims as they happen * **Infinite Scroll**: Load more events as the user scrolls ### Time Mode * **Weekly/Monthly Reports**: Generate reports for specific time periods * **Analytics Dashboards**: Show claim activity over custom date ranges * **Auditing**: Review all claims that occurred during a specific period * **Data Sync**: Sync historical claim data to your database ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. Query claim events and stats directly from the terminal: **Offset-based pagination:** ```bash theme={null} bags fees events TOKEN_MINT --offset 0 --limit 10 ``` **Time-based filtering:** ```bash theme={null} bags fees events TOKEN_MINT --start-time 2025-01-01T00:00:00Z --end-time 2025-06-01T00:00:00Z ``` **Aggregated claim stats:** ```bash theme={null} bags fees stats TOKEN_MINT ``` Use `--json` to pipe the output into other tools for analysis: ```bash theme={null} bags fees events TOKEN_MINT --limit 100 --json | jq '.[] | .amount' ``` ## Error Handling Common errors to handle: | Error | Cause | Solution | | ---------------- | -------------------------- | -------------------------------------------- | | 400 Bad Request | Invalid `tokenMint` format | Verify the mint is a valid base58 public key | | 400 Bad Request | `from` > `to` in time mode | Ensure `from` is less than or equal to `to` | | 401 Unauthorized | Missing or invalid API key | Check your API key configuration | When using time mode, the `from` timestamp must be less than or equal to `to`. The API validates this constraint and returns an error if violated. ## Related Guides * [Get Token Lifetime Fees](/how-to-guides/get-token-lifetime-fees) - Get total fees earned by a token * [Get Token Creators](/how-to-guides/get-token-creators) - Find token launch creators * [Claim Fees](/how-to-guides/claim-fees) - Claim your earned fees # Get Token Creators Source: https://docs.bags.fm/how-to-guides/get-token-creators Learn how to retrieve token launch creators/deployers with provider details using the Bags TypeScript SDK and Node.js In this guide, you'll learn how to retrieve token launch creators/deployers using the Bags TypeScript SDK with Node.js. This includes provider, display name, wallet address, royalty percentage, and profile image. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * The token mint address you want to analyze. ## 1. The Token Creators Script Here is a comprehensive script to fetch token creator information. Save this as `get-token-creators.ts`. This script retrieves detailed information about the primary creator and any additional launch participants, including provider, display name, wallet, profile picture, and royalty percentage. ## Endpoint Used Under the Hood This guide uses: * [`GET /token-launch/creator/v3`](/api-reference/get-token-launch-creators) via `sdk.state.getTokenCreators()` ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK } from "@bagsfm/bags-sdk"; import { PublicKey, Connection } from "@solana/web3.js"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; if (!BAGS_API_KEY || !SOLANA_RPC_URL) { throw new Error("BAGS_API_KEY and SOLANA_RPC_URL are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function getTokenCreators(tokenMint: string) { try { console.log(`🔍 Fetching token creators for: ${tokenMint}`); const creators = await sdk.state.getTokenCreators(new PublicKey(tokenMint)); console.log(`📊 Found ${creators.length} creator(s)/deployer(s)`); const primaryCreator = creators.find(c => c.isCreator); if (!primaryCreator) { throw new Error("❌ No primary creator found for this token!"); } console.log("✨ Primary Creator Details:"); console.log(`👤 Display Name: ${primaryCreator.providerUsername ?? primaryCreator.username ?? "N/A"}`); console.log(`🔗 Provider: ${primaryCreator.provider ?? "unknown"}`); console.log(`👛 Wallet: ${primaryCreator.wallet}`); console.log(`🎨 Profile Picture: ${primaryCreator.pfp}`); console.log(`💰 Royalty: ${primaryCreator.royaltyBps / 100}%`); const others = creators.filter(c => !c.isCreator); if (others.length > 0) { console.log("\n🤝 Other Launch Participants:"); others.forEach((participant, idx) => { console.log(`\n#${idx + 1}`); console.log(`👤 Display Name: ${participant.providerUsername ?? participant.username ?? "N/A"}`); console.log(`🔗 Provider: ${participant.provider ?? "unknown"}`); console.log(`👛 Wallet: ${participant.wallet}`); console.log(`🎨 Profile Picture: ${participant.pfp}`); console.log(`💰 Royalty: ${participant.royaltyBps / 100}%`); }); } else { console.log("\n📝 No additional launch participants found for this token"); } console.log("\n✅ Successfully retrieved token creators!"); } catch (error) { console.error("🚨 Error fetching token creators:", error); } } getTokenCreators("CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS"); ``` ## 2. Run Your Script To analyze a token's creators, edit the `getTokenCreators` function call at the bottom of the script with the actual mint address you want to analyze. Then, run the script from your terminal: ```bash theme={null} npx ts-node get-token-creators.ts ``` ## What You'll See The script will output detailed information about the token's launch creators/deployers: Example output: ``` 🔍 Fetching token creators for: CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS 📊 Found 2 creator(s)/deployer(s) ✨ Primary Creator Details: 👤 Display Name: tokenCreatorOnTwitter 🔗 Provider: twitter 👛 Wallet: 8xX...abc 🎨 Profile Picture: https://example.com/pfp.jpg 💰 Royalty: 5% 🤝 Other Launch Participants: #1 👤 Display Name: feeShareUser 🔗 Provider: github 👛 Wallet: 3yz...def 🎨 Profile Picture: https://example.com/pfp2.jpg 💰 Royalty: 2% ✅ Successfully retrieved token creators! ``` ## Understanding the Data * **isCreator**: Identifies the primary token creator. There can be multiple creators/deployers, but at most one will have `isCreator: true`. * **provider**: The social/login provider associated with the creator (e.g., `twitter`, `tiktok`, `kick`, `github`). Use this to show a platform logo. May be `unknown` or `null`. * **providerUsername**: The username on the provider platform, when available. Prefer this for display. The plain `username` is a Bags internal username and may be absent. * **wallet**: The creator's wallet address. * **pfp**: URL to the profile image, when available. * **royaltyBps**: Royalty in basis points. Divide by 100 to display as a percentage. * **twitterUsername** *(optional)*: The creator's Twitter/X username, if available. * **bagsUsername** *(optional)*: The creator's Bags platform username, if available. * **isAdmin** *(optional)*: Whether this user is an admin of the token. ### Key Information Retrieved: * **Display Name**: `providerUsername` if present, otherwise `username` * **Provider**: Source platform for displaying logos and context * **Wallet**: Wallet address string * **Profile Picture**: URL string * **Royalty**: Percentage derived from `royaltyBps / 100` ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. Get token creators directly from the terminal: ```bash theme={null} bags launch creators --mint TOKEN_MINT_ADDRESS ``` Use `--json` for machine-readable output: ```bash theme={null} bags launch creators --mint TOKEN_MINT_ADDRESS --json ``` ## Use Cases This information is valuable for: * **Due Diligence**: Research token creators before investing * **Creator Discovery**: Find and follow successful token creators * **Fee Analysis**: Understand how token fees are distributed * **Partnership Opportunities**: Identify potential collaborators * **Portfolio Research**: Learn about the teams behind your investments For token performance metrics, also check out our [Get Token Lifetime Fees](/how-to-guides/get-token-lifetime-fees) guide. # Get Token Lifetime Fees Source: https://docs.bags.fm/how-to-guides/get-token-lifetime-fees Learn how to retrieve the total lifetime fees earned by a Solana token using the Bags API with TypeScript and Node.js In this guide, you'll learn how to retrieve the total lifetime fees earned by a Solana token using the Bags TypeScript SDK with Node.js. This is useful for tracking token performance and earnings over time. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * The token mint address you want to analyze. ## 1. The Token Lifetime Fees Script Here is a simple script to fetch token lifetime fees. Save this as `get-token-lifetime-fees.ts`. This script uses the updated SDK API to retrieve lifetime fees for any token and displays them in a user-friendly format. ## Endpoint Used Under the Hood This guide uses: * [`GET /token-launch/lifetime-fees`](/api-reference/get-token-lifetime-fees) via `sdk.state.getTokenLifetimeFees()` ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK } from "@bagsfm/bags-sdk"; import { LAMPORTS_PER_SOL, PublicKey, Connection } from "@solana/web3.js"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; if (!BAGS_API_KEY || !SOLANA_RPC_URL) { throw new Error("BAGS_API_KEY and SOLANA_RPC_URL are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function getTokenLifetimeFees(tokenMint: string) { const feesLamports = await sdk.state.getTokenLifetimeFees(new PublicKey(tokenMint)); console.log("💰 Token lifetime fees:", (feesLamports / LAMPORTS_PER_SOL).toLocaleString(), "SOL"); } getTokenLifetimeFees("CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS"); ``` ## 2. Run Your Script To analyze a token's lifetime fees, edit the `getTokenLifetimeFees` function call at the bottom of the script with the actual mint address you want to analyze. Then, run the script from your terminal: ```bash theme={null} npx ts-node get-token-lifetime-fees.ts ``` ## What You'll See The script will output the total lifetime fees earned by the token in SOL, formatted with proper number localization for easy reading. Example output: ``` 💰 Token lifetime fees: 1,234.567890 SOL ``` ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. Get lifetime fees for a token directly from the terminal: ```bash theme={null} bags fees lifetime TOKEN_MINT_ADDRESS ``` To display the result in raw lamports instead of SOL: ```bash theme={null} bags fees lifetime TOKEN_MINT_ADDRESS --raw ``` ## Use Cases This information is valuable for: * **Token Performance Analysis**: Track how much revenue a token has generated * **Investment Research**: Evaluate token success based on fee generation * **Portfolio Management**: Monitor earnings from tokens you've created or invested in * **Market Analysis**: Compare performance across different tokens For more advanced token analytics, check out our [Get Token Creators](/how-to-guides/get-token-creators) guide. # Initial Buy Math Source: https://docs.bags.fm/how-to-guides/initial-buy-math Calculate how many tokens you receive for your initial buy using the Meteora Dynamic Bonding Curve SDK In this guide, you'll learn how to estimate the initial buy token amount you receive when buying with SOL. This is useful to understand what percentage of the supply you would get for a given SOL amount before executing a buy. ## Prerequisites Before starting, make sure you have: * Node.js (v18 or higher) and npm. * A Solana RPC URL (e.g. from [Helius](https://helius.dev), or a public endpoint). * Installed the required dependencies: ```bash theme={null} npm install @meteora-ag/dynamic-bonding-curve-sdk @solana/web3.js bn.js ``` ## 1. Set Your RPC and Curve Parameters Create a file (e.g. `estimate-allocation.ts`) and set your RPC URL. ## 2. Token Allocation Script The following script uses the Meteora Dynamic Bonding Curve SDK to get a **swap quote** for a given SOL amount (in lamports). The function returns the **token amount** you would receive (in token units, 9 decimals). You can then compute the percentage of supply as `(tokenAmount / totalSupply) * 100` if you have the token's total supply. ```typescript theme={null} import { DynamicBondingCurveClient } from '@meteora-ag/dynamic-bonding-curve-sdk'; import { BaseFeeMode, CollectFeeMode } from '@meteora-ag/dynamic-bonding-curve-sdk'; import { Connection } from '@solana/web3.js'; import BN from 'bn.js'; const RPC_URL = ""; const SOLANA_CONNECTION = new Connection(RPC_URL); const BONDING_CURVE_CLIENT = new DynamicBondingCurveClient(SOLANA_CONNECTION, 'processed'); const SQRT_START_PRICE = new BN('3141367320245630'); const MIGRATION_QUOTE_THRESHOLD = new BN(85000000000); const BASE_FEE_CLIFF_FEE_NUMERATOR = new BN('20000000'); const BASE_FEE_FIRST_FACTOR = 0; const BASE_FEE_SECOND_FACTOR = new BN('0'); const BASE_FEE_THIRD_FACTOR = new BN('0'); const BASE_FEE_MODE = BaseFeeMode.FeeSchedulerLinear; const COLLECT_FEE_MODE = CollectFeeMode.QuoteToken; const CURVE = [ { sqrtPrice: new BN('6401204812200420'), liquidity: new BN('3929368168768468756200000000000000'), }, { sqrtPrice: new BN('13043817825332782'), liquidity: new BN('2425988008058820449100000000000000'), }, ]; export const getOutputAmountOfInitialBuy = async (initialBuyIn: string): Promise => { try { const quote = await BONDING_CURVE_CLIENT.pool.swapQuote({ virtualPool: { quoteReserve: new BN(0), sqrtPrice: SQRT_START_PRICE, activationPoint: new BN(0), volatilityTracker: { volatilityAccumulator: new BN(0), }, } as any, config: { collectFeeMode: COLLECT_FEE_MODE, migrationQuoteThreshold: MIGRATION_QUOTE_THRESHOLD, poolFees: { baseFee: { cliffFeeNumerator: BASE_FEE_CLIFF_FEE_NUMERATOR, firstFactor: BASE_FEE_FIRST_FACTOR, secondFactor: BASE_FEE_SECOND_FACTOR, thirdFactor: BASE_FEE_THIRD_FACTOR, baseFeeMode: BASE_FEE_MODE, }, dynamicFee: { initilized: new BN(0), }, }, curve: [ { sqrtPrice: CURVE[0].sqrtPrice, liquidity: CURVE[0].liquidity, }, { sqrtPrice: CURVE[1].sqrtPrice, liquidity: CURVE[1].liquidity, }, ], } as any, swapBaseForQuote: false, amountIn: new BN(initialBuyIn), slippageBps: 0, hasReferral: false, currentPoint: new BN(0), }); return quote.outputAmount.div(new BN(10 ** 9)).toString(); } catch (err) { console.error(err); return null; } }; ``` ## 3. Using the Function * **Input**: `initialBuyIn` is the SOL amount in **lamports** (1 SOL = 1,000,000,000 lamports). * **Output**: The function returns the token amount you receive (in token base units, 9 decimals), or `null` on error. Example: to see how many tokens you get for 1 SOL: ```typescript theme={null} const tokenAmount = await getOutputAmountOfInitialBuy((1e9).toString()); // 1 SOL in lamports console.log("Tokens received:", tokenAmount); // To get % of supply: (Number(tokenAmount) / totalSupply) * 100 ``` # Launch a Token Source: https://docs.bags.fm/how-to-guides/launch-token Complete step-by-step guide to launch a Solana token using the Bags API v2 with TypeScript and Node.js In this guide, you'll learn how to launch a Solana token using the Bags TypeScript SDK with Node.js. Token Launch v2 requires fee sharing configuration, where all fees are shared with the creator's wallet by default. You can optionally share fees with additional fee claimers. Want to quote your token in a non-SOL asset (xStocks, Ondo tokenized equities, and other badged mints) instead of SOL? See [Launch a Token with a Non-SOL Quote Token](/how-to-guides/launch-token-non-sol-quote). ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with some SOL for transactions. * A token image URL (recommended) or image file. * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` **Optional**: If you want to include a partner configuration in your token launch, you'll need to create a partner key first. See the [Create Partner Key](/how-to-guides/create-partner-key) guide for details. Once you have a partner key, you can include it when launching tokens using the `partner` and `partnerConfig` parameters. ## 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ## 2. The Token Launch Script Here is the complete script for launching a token. Save it as `launch-token.ts`. The script follows the Token Launch v2 flow: 1. Create metadata 2. Create config (fee share configuration) 3. Get token creation transaction 4. Sign transaction 5. Broadcast transaction ## Endpoints Used Under the Hood This SDK flow calls these documented endpoints: * [`POST /token-launch/create-token-info`](/api-reference/create-token-info) via `sdk.tokenLaunch.createTokenInfoAndMetadata()` * [`GET /token-launch/fee-share/wallet/v2`](/api-reference/get-fee-share-wallet) via `sdk.state.getLaunchWalletV2()` for social username fee-claimer lookup * [`POST /fee-share/config`](/api-reference/create-fee-share-configuration) via `sdk.config.createBagsFeeShareConfig()` * [`POST /token-launch/create-launch-transaction`](/api-reference/create-token-launch-transaction) via `sdk.tokenLaunch.createLaunchTransaction()` This guide also uses SDK Solana helper methods for Jito bundle/tip workflows. Those helper endpoints are currently undocumented in this docs set, so they are intentionally not linked here. ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, BAGS_FEE_SHARE_V2_MAX_CLAIMERS_NON_LUT, waitForSlotsToPass, signAndSendTransaction, createTipTransaction, sendBundleAndConfirm, } from "@bagsfm/bags-sdk"; import type { SupportedSocialProvider } from "@bagsfm/bags-sdk"; import { Keypair, LAMPORTS_PER_SOL, PublicKey, Connection, VersionedTransaction } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); const FALLBACK_JITO_TIP_LAMPORTS = 0.015 * LAMPORTS_PER_SOL; /** * Create a tip transaction, sign bundle transactions, and send via Jito * @param unsignedTransactions - Array of unsigned VersionedTransaction instances * @param keypair - The keypair to sign all transactions * @returns The confirmed bundle ID */ async function sendBundleWithTip( unsignedTransactions: VersionedTransaction[], keypair: Keypair ): Promise { const commitment = sdk.state.getCommitment(); // Get blockhash from the first bundle transaction const bundleBlockhash = unsignedTransactions[0]?.message.recentBlockhash; if (!bundleBlockhash) { throw new Error("Bundle transactions must have a blockhash"); } let jitoTip = FALLBACK_JITO_TIP_LAMPORTS; // Get recommended Jito tip const recommendedJitoTip = await sdk.solana.getJitoRecentFees().catch((err) => { console.log("⚠️ Failed to get Jito recent fees, using fallback:", err.message); return null; }); // Calculate tip amount (use 95th percentile or fallback to default) if (recommendedJitoTip?.landed_tips_95th_percentile) { jitoTip = Math.floor(recommendedJitoTip.landed_tips_95th_percentile * LAMPORTS_PER_SOL); } console.log(`💰 Jito tip: ${jitoTip / LAMPORTS_PER_SOL} SOL`); // Create tip transaction const tipTransaction = await createTipTransaction(connection, commitment, keypair.publicKey, jitoTip, { blockhash: bundleBlockhash, }); // Sign all transactions (tip first, then the rest) const signedTransactions = [tipTransaction, ...unsignedTransactions].map((tx) => { tx.sign([keypair]); return tx; }); console.log(`📦 Sending bundle via Jito...`); // Send bundle and wait for confirmation const bundleId = await sendBundleAndConfirm(signedTransactions, sdk); console.log(`✅ Bundle confirmed! Bundle ID: ${bundleId}`); return bundleId; } async function getOrCreateFeeShareConfig( tokenMint: PublicKey, creatorWallet: PublicKey, keypair: Keypair, feeClaimers: Array<{ user: PublicKey; userBps: number }>, partner?: PublicKey, // Optional: Partner wallet address partnerConfig?: PublicKey // Optional: Partner config PDA (see Create Partner Key guide) ): Promise { const commitment = sdk.state.getCommitment(); // Check if lookup tables are needed (when there are more than MAX_CLAIMERS_NON_LUT claimers) let additionalLookupTables: PublicKey[] | undefined; if (feeClaimers.length > BAGS_FEE_SHARE_V2_MAX_CLAIMERS_NON_LUT) { console.log(`📋 Creating lookup tables for ${feeClaimers.length} fee claimers (exceeds ${BAGS_FEE_SHARE_V2_MAX_CLAIMERS_NON_LUT} limit)...`); // Get LUT creation transactions const lutResult = await sdk.config.getConfigCreationLookupTableTransactions({ payer: creatorWallet, baseMint: tokenMint, feeClaimers: feeClaimers, }); if (!lutResult) { throw new Error("Failed to create lookup table transactions"); } // Execute the LUT creation transaction first console.log("🔧 Executing lookup table creation transaction..."); await signAndSendTransaction(connection, commitment, lutResult.creationTransaction, keypair); // Wait for one slot to pass (required before extending LUT) console.log("⏳ Waiting for one slot to pass..."); await waitForSlotsToPass(connection, commitment, 1); // Execute all extend transactions console.log(`🔧 Executing ${lutResult.extendTransactions.length} lookup table extend transaction(s)...`); for (const extendTx of lutResult.extendTransactions) { await signAndSendTransaction(connection, commitment, extendTx, keypair); } additionalLookupTables = lutResult.lutAddresses; console.log("✅ Lookup tables created successfully!"); } try { // Try to create the config (with LUTs if needed) const configResult = await sdk.config.createBagsFeeShareConfig({ payer: creatorWallet, baseMint: tokenMint, feeClaimers: feeClaimers, partner: partner, partnerConfig: partnerConfig, additionalLookupTables: additionalLookupTables, }); console.log("🔧 Creating fee share config..."); // Send bundle txs if (configResult.bundles && configResult.bundles.length > 0) { console.log(`📦 Sending ${configResult.bundles.length} bundle(s) via Jito...`); for (const bundle of configResult.bundles) { // Send the bundle with tip transaction and wait for confirmation await sendBundleWithTip(bundle, keypair); } } // Sign and send all returned transactions for (const tx of configResult.transactions || []) { await signAndSendTransaction(connection, commitment, tx, keypair); } console.log("✅ Fee share config created successfully!"); return configResult.meteoraConfigKey; } catch (error: any) { console.error("🚨 Failed getting or creating fee share config:", error); throw error; } } async function launchToken(launchParams: { imageUrl: string; name: string; symbol: string; description: string; twitterUrl?: string; websiteUrl?: string; telegramUrl?: string; initialBuyAmountLamports: number; // Optional: Share fees with fee claimers // Each entry should have provider, username, and the percentage (bps) they receive feeClaimers?: Array<{ provider: SupportedSocialProvider; username: string; bps: number; // Basis points (10000 = 100%) }>; // Optional: Partner configuration for fee sharing // See the Create Partner Key guide for details: /how-to-guides/create-partner-key partner?: PublicKey; // Partner wallet address partnerConfig?: PublicKey; // Partner config PDA (can be derived using deriveBagsFeeShareV2PartnerConfigPda) }) { try { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); const commitment = sdk.state.getCommitment(); console.log(`🚀 Creating token $${launchParams.symbol} with wallet ${keypair.publicKey.toBase58()}`); // Step 1: Create metadata console.log("📝 Step 1: Creating token info and metadata..."); const tokenInfoResponse = await sdk.tokenLaunch.createTokenInfoAndMetadata({ imageUrl: launchParams.imageUrl, name: launchParams.name, description: launchParams.description, symbol: launchParams.symbol?.toUpperCase()?.replace("$", ""), twitter: launchParams.twitterUrl, website: launchParams.websiteUrl, telegram: launchParams.telegramUrl, }); console.log("✨ Successfully created token info and metadata!"); console.log("🪙 Token mint:", tokenInfoResponse.tokenMint); // Step 2: Get or create fee share config console.log("⚙️ Step 2: Getting or creating fee share config..."); const tokenMint = new PublicKey(tokenInfoResponse.tokenMint); // Build fee claimers array // IMPORTANT: Creator must always be included explicitly with their BPS set let feeClaimers: Array<{ user: PublicKey; userBps: number }> = []; if (launchParams.feeClaimers && launchParams.feeClaimers.length > 0) { // Calculate creator's share (remaining after all fee claimers) const feeClaimersBps = launchParams.feeClaimers.reduce((sum, fc) => sum + fc.bps, 0); const creatorBps = 10000 - feeClaimersBps; if (creatorBps < 0) { throw new Error("Total fee claimer BPS cannot exceed 10000 (100%)"); } // Add creator first with explicit BPS (required - creator must always be explicit) if (creatorBps > 0) { feeClaimers.push({ user: keypair.publicKey, userBps: creatorBps }); console.log(`💰 Creator will receive ${creatorBps / 100}% of fees (explicitly set)`); } // Add fee claimers for (const feeClaimer of launchParams.feeClaimers) { console.log( `🔍 Looking up fee claimer wallet for ${feeClaimer.provider}:${feeClaimer.username}` ); const feeClaimerResult = await sdk.state.getLaunchWalletV2( feeClaimer.username, feeClaimer.provider ); feeClaimers.push({ user: feeClaimerResult.wallet, userBps: feeClaimer.bps, }); console.log( `✨ Found fee claimer wallet: ${feeClaimerResult.wallet.toString()} (${feeClaimer.bps / 100}%)` ); } } else { // No fee claimers - creator gets all fees (must be set explicitly to max BPS) console.log("💰 All fees will go to creator wallet (explicitly set to 10000 bps)"); feeClaimers = [{ user: keypair.publicKey, userBps: 10000 }]; } const configKey = await getOrCreateFeeShareConfig( tokenMint, keypair.publicKey, keypair, feeClaimers, launchParams.partner, launchParams.partnerConfig ); console.log("🔑 Config Key:", configKey.toString()); // Step 3: Get token creation transaction console.log("🎯 Step 3: Creating token launch transaction..."); const tokenLaunchTransaction = await sdk.tokenLaunch.createLaunchTransaction({ metadataUrl: tokenInfoResponse.tokenMetadata, tokenMint: tokenMint, launchWallet: keypair.publicKey, initialBuyLamports: launchParams.initialBuyAmountLamports, configKey: configKey, }); // Step 4 & 5: Sign and broadcast transaction console.log("📡 Step 4 & 5: Signing and broadcasting transaction..."); const signature = await signAndSendTransaction(connection, commitment, tokenLaunchTransaction, keypair); console.log("🎉 Token launched successfully!"); console.log("🪙 Token Mint:", tokenInfoResponse.tokenMint); console.log("🔑 Launch Signature:", signature); console.log("📄 Metadata URI:", tokenInfoResponse.tokenMetadata); console.log(`🌐 View your token at: https://bags.fm/${tokenInfoResponse.tokenMint}`); } catch (error) { console.error("🚨 Token launch failed:", error); throw error; } } // Example: Launch token with shared fees among multiple users // (40% creator, 30% fee claimer 1, 30% fee claimer 2) launchToken({ imageUrl: "https://img.freepik.com/premium-vector/white-abstract-vactor-background-design_665257-153.jpg", name: "Multi-Share Token", symbol: "MST", description: "This token shares fees with multiple fee claimers", twitterUrl: "https://x.com/multisharetoken", websiteUrl: "https://multisharetoken.com", initialBuyAmountLamports: 0.01 * LAMPORTS_PER_SOL, feeClaimers: [ { provider: "twitter", username: "feeclaimer1", bps: 3000, // 30% to first fee claimer }, { provider: "twitter", username: "feeclaimer2", bps: 3000, // 30% to second fee claimer }, // Creator automatically gets remaining 40% ], }); // Example: Launch token without sharing fees (all fees go to creator) // launchToken({ // imageUrl: "https://img.freepik.com/premium-vector/white-abstract-vactor-background-design_665257-153.jpg", // name: "My Token", // symbol: "MTK", // description: "This is my token description", // twitterUrl: "https://x.com/mytoken", // websiteUrl: "https://mytoken.com", // initialBuyAmountLamports: 0.01 * LAMPORTS_PER_SOL, // 0.01 SOL // }); ``` ## 3. Understanding Fee Sharing Token Launch v2 requires fee sharing configuration with explicit BPS (basis points) allocation. **Important rules:** ### Key Rules 1. **Creators must always explicitly set their BPS**: To receive all fees, creators must give themselves the maximum BPS (10000) explicitly, no matter what. Creator fees must always be set explicitly in the fee claimers array. 2. **When sharing fees**: Give fee claimers their BPS and then **explicitly also give the creator their BPS**. Both the creator and all fee claimers must have their BPS values set explicitly in the configuration. 3. **Total BPS must equal 10000**: The sum of all BPS values (creator + all fee claimers) must equal exactly 10,000 (100%). 4. **Maximum fee earners**: You can have up to 100 fee earners (including the creator) per token launch. 5. **Supported platforms**: Fee claimers can be identified using supported social platforms: `twitter`, `kick`, and `github`. 6. **Lookup Tables (LUTs)**: When you have more than 15 fee claimers, you need to create lookup tables before creating the fee share config. The script automatically handles this by: * Calling `getConfigCreationLookupTableTransactions()` to get LUT creation transactions * Executing the LUT creation transaction * Waiting for one slot to pass (required by Solana) * Executing all LUT extend transactions * Passing the LUT addresses to `createBagsFeeShareConfig` via `additionalLookupTables` ### Partner Configuration You can include a partner configuration in your fee share setup by providing: * `partner`: The partner wallet address (PublicKey) * `partnerConfig`: The partner config PDA (PublicKey) - can be derived using the helper function shown in the examples Partners receive a share of fees from token launches that include their partner configuration. This is useful for platforms or partnerships that want to collect fees from multiple token launches. To create a partner key, see the [Create Partner Key](/how-to-guides/create-partner-key) guide. ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. The CLI wraps the entire multi-step launch flow (metadata upload, fee share config, lookup tables, Jito bundles, and launch transaction) into a single command: **Launch with all fees to creator:** ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "A great token" \ --image-url "https://example.com/image.png" \ --initial-buy 10000000 \ --skip-confirm ``` **Launch with fee sharing:** ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "A great token" \ --image-url "https://example.com/image.png" \ --initial-buy 10000000 \ --fee-claimers '[{"provider":"twitter","username":"ramyobags","bps":3000},{"provider":"twitter","username":"bob","bps":2000}]' \ --skip-confirm ``` The creator automatically receives the remaining BPS (in the example above, 50%). **Launch with a partner:** ```bash theme={null} bags launch create \ --name "My Token" \ --symbol "MTK" \ --description "A great token" \ --image-url "https://example.com/image.png" \ --initial-buy 10000000 \ --partner PARTNER_WALLET_PUBKEY \ --partner-config PARTNER_CONFIG_PDA \ --skip-confirm ``` If you omit the flags, the CLI guides you through an interactive wizard that lets you add fee claimers one at a time (by social username or wallet address) and set their BPS allocation. ## 4. Run Your Script To launch your token, edit the `launchToken` function call at the bottom of `launch-token.ts` with your token's details, especially the `imageUrl`. Then, run the script from your terminal: ```bash theme={null} npx ts-node launch-token.ts ``` ## 5. Troubleshooting The script includes comprehensive error handling. Common issues include: * **API Key Issues**: Ensure your API key is valid. * **Private Key Format**: Your private key must be base58 encoded. * **Insufficient SOL**: Your wallet needs SOL for transaction fees. * **Image URL**: The URL to your token image must be accessible and valid. * **Invalid Fee Claimer**: Ensure the fee claimer provider and username are valid and the user has a registered wallet. For more details, see the [API Reference](/api-reference/introduction). # Launch a Token with a Non-SOL Quote Token Source: https://docs.bags.fm/how-to-guides/launch-token-non-sol-quote Launch a Solana token directly into a Meteora DAMM v2 pool quoted in a non-SOL token (xStocks, Ondo tokenized equities, and other badged mints) using the Bags TypeScript SDK. Most Bags launches are quoted in SOL and start on a DBC bonding curve that later graduates to a DAMM v2 pool. A **non-SOL quote launch** is different: the token is created straight into a single-sided Meteora DAMM v2 pool quoted in a badged non-SOL mint (for example an xStock or an Ondo tokenized equity). There is no bonding curve and no migration step. ## How it differs from a standard launch * **No DBC bonding curve and no migration.** The pool exists and is fully seeded the moment the launch transaction lands. * **No fee share config key.** You do not call the fee share config endpoint. Fee routing is handled by the launch itself (fee-share position custody). * **Fees are collected in the quote token, not SOL.** A flat 2% base fee accrues in the pool's quote mint. All amounts (initial buy, claimable fees, vault balances) are denominated in that quote mint's base units. * **Liquidity is permanently locked.** The launch creates two positions (50% / 50%) that are permanently locked and deposited into fee-share position custody. The creator claims accrued fees, not liquidity. Standard SOL-quoted launches are covered in [Launch a Token](/how-to-guides/launch-token). This guide is only for launching directly into a DAMM v2 pool with a non-SOL quote token. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with some SOL for transaction fees and rent. * If you want to perform an initial buy at launch, a balance of the **quote token** in your wallet (the initial buy is spent in the quote token, not SOL). * A token image URL (recommended) or image file. * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` ## 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ## 2. Pick a Quote Token Non-SOL launches must use a supported (badged) quote mint. Fetch the current list and pick one. Each entry includes the mint, its owning token program, decimals, and metadata. ```typescript theme={null} const quoteTokens = await sdk.tokenLaunch.getDammV2SupportedQuoteTokens(); for (const token of quoteTokens) { console.log(`${token.symbol ?? "(no symbol)"} — ${token.mint} (${token.decimals} decimals)`); } ``` Quote mint decimals vary per token (for example, xStocks use 8 decimals and Ondo mints use 9). The `initialBuyQuoteAmount` you pass later is in the quote mint's **base units**, so always use the `decimals` value returned here to convert from a human amount. ## 3. The Launch Script Here is the complete script. Save it as `launch-token-non-sol-quote.ts`. The flow is: 1. Pick a supported quote token 2. Create metadata (`createTokenInfoAndMetadata`) 3. Build the launch transaction bundle (`createDammV2LaunchTransaction`) 4. Sign and submit the bundle in order ## Endpoints Used Under the Hood This SDK flow calls these documented endpoints: * [`GET /token-launch/damm-v2/supported-quote-tokens`](/api-reference/get-damm-v2-supported-quote-tokens) via `sdk.tokenLaunch.getDammV2SupportedQuoteTokens()` * [`POST /token-launch/create-token-info`](/api-reference/create-token-info) via `sdk.tokenLaunch.createTokenInfoAndMetadata()` * [`POST /token-launch/damm-v2/create-transaction`](/api-reference/create-damm-v2-launch-transaction) via `sdk.tokenLaunch.createDammV2LaunchTransaction()` * [`GET /token-launch/damm-v2/launches`](/api-reference/get-damm-v2-launches) via `sdk.tokenLaunch.getDammV2Launches()` ```typescript theme={null} // launch-token-non-sol-quote.ts import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, signAndSendTransaction } from "@bagsfm/bags-sdk"; import { Keypair, Connection, PublicKey, VersionedTransaction } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); // The launch bundle is returned as ordered, base58-encoded transactions with a `type` // label. LUT_SETUP, CREATE_TOKEN, and LAUNCH complete the launch; LUT_DEACTIVATE and // LUT_CLOSE are optional cleanup that reclaim the lookup-table rent afterwards. const CORE_LAUNCH_STEPS = ["LUT_SETUP", "CREATE_TOKEN", "LAUNCH"] as const; async function launchToken(launchParams: { imageUrl: string; name: string; symbol: string; description: string; quoteMint: string; // From getDammV2SupportedQuoteTokens() twitterUrl?: string; websiteUrl?: string; telegramUrl?: string; // Optional: initial buy amount, in the quote mint's BASE UNITS (not SOL, not whole tokens) initialBuyQuoteAmount?: number; // Optional: wallet that receives the creator fee position (defaults to the launch wallet) feeClaimerWallet?: PublicKey; // Optional: an existing partner config wallet to attach to the custody partner?: PublicKey; }) { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); const commitment = sdk.state.getCommitment(); console.log(`🚀 Launching $${launchParams.symbol} with wallet ${keypair.publicKey.toBase58()}`); // Step 1: Create metadata console.log("📝 Step 1: Creating token info and metadata..."); const tokenInfo = await sdk.tokenLaunch.createTokenInfoAndMetadata({ imageUrl: launchParams.imageUrl, name: launchParams.name, description: launchParams.description, symbol: launchParams.symbol?.toUpperCase()?.replace("$", ""), twitter: launchParams.twitterUrl, website: launchParams.websiteUrl, telegram: launchParams.telegramUrl, }); console.log("🪙 Token mint:", tokenInfo.tokenMint); // Step 2: Build the DAMM v2 direct launch bundle console.log("🎯 Step 2: Building the launch transaction bundle..."); const { transactions, launch } = await sdk.tokenLaunch.createDammV2LaunchTransaction({ metadataUrl: tokenInfo.tokenMetadata, tokenMint: new PublicKey(tokenInfo.tokenMint), wallet: keypair.publicKey, quoteMint: new PublicKey(launchParams.quoteMint), feeClaimerWallet: launchParams.feeClaimerWallet, initialBuyQuoteAmount: launchParams.initialBuyQuoteAmount, partner: launchParams.partner, }); console.log("🏊 Pool:", launch.pool); console.log("💵 Launch price (quote per token):", launch.launchPriceQuotePerToken); console.log("📊 Implied launch FDV (USD):", launch.impliedLaunchFdvUsd); // Step 3: Sign and submit the bundle IN ORDER. // Each item is a base58-encoded transaction already partially signed by the server; // your wallet co-signs. Submit LUT_SETUP, then CREATE_TOKEN, then LAUNCH. console.log("📡 Step 3: Signing and submitting the launch bundle..."); for (const step of CORE_LAUNCH_STEPS) { const item = transactions.find((tx) => tx.type === step); if (!item) continue; // CREATE_TOKEN is skipped on rebuilds where the mint already landed const transaction = VersionedTransaction.deserialize(bs58.decode(item.transaction)); const signature = await signAndSendTransaction(connection, commitment, transaction, keypair); console.log(`✅ ${step} confirmed: ${signature}`); } console.log("🎉 Token launched successfully!"); console.log(`🌐 View your token at: https://bags.fm/${tokenInfo.tokenMint}`); return { tokenMint: tokenInfo.tokenMint, launch }; } // Example: launch a token quoted in a non-SOL badged mint, with no initial buy. // Replace quoteMint with a mint returned by getDammV2SupportedQuoteTokens(). launchToken({ imageUrl: "https://img.freepik.com/premium-vector/white-abstract-vactor-background-design_665257-153.jpg", name: "My Equity Token", symbol: "MET", description: "A token quoted in a non-SOL asset", quoteMint: "REPLACE_WITH_SUPPORTED_QUOTE_MINT", }); ``` ### Understanding the launch bundle `createDammV2LaunchTransaction` returns: * **`transactions`** — an ordered array of `{ type, transaction }`, where `transaction` is a base58-encoded, partially-signed transaction. The `type` is one of: * `LUT_SETUP` — creates the address lookup table the launch transaction depends on. * `CREATE_TOKEN` — creates the SPL mint and metadata (omitted on rebuilds where the mint already exists). * `LAUNCH` — the single atomic transaction that creates the pool, locks both positions, deposits them into custody, and performs the optional initial buy. * `LUT_DEACTIVATE` / `LUT_CLOSE` — optional cleanup to reclaim the lookup-table rent after the launch (see below). * **`launch`** — details about the launch: `pool`, `quoteMint`, `quoteTokenProgram`, `treasuryPositionNftMint`, `feeClaimerPositionNftMint`, `feeClaimerWallet`, `lookupTable`, `positionCustody`, `custodyAuthority`, `launchPriceQuotePerToken`, and `impliedLaunchFdvUsd`. Unlike the classic SOL launch helper, `createDammV2LaunchTransaction` returns base58 strings (not `VersionedTransaction` objects). Deserialize each with `VersionedTransaction.deserialize(bs58.decode(item.transaction))` before signing, as shown above. ### Optional: reclaim the lookup-table rent The `LUT_SETUP` step pays rent for an address lookup table. After the launch confirms you can reclaim that rent with the `LUT_DEACTIVATE` and `LUT_CLOSE` transactions. These are not server-signed and use a fixed blockhash, so refresh the blockhash before signing, and note that `LUT_CLOSE` is only valid roughly 513 slots after `LUT_DEACTIVATE` lands. This cleanup is optional and can be skipped. ## 4. Verify the Launch Confirmed launches appear in the DAMM v2 direct launches feed. You can filter by the quote mint you used: ```typescript theme={null} const { launches } = await sdk.tokenLaunch.getDammV2Launches({ quoteMint: new PublicKey("REPLACE_WITH_SUPPORTED_QUOTE_MINT"), limit: 20, }); console.log(`Found ${launches.length} launch(es)`); ``` ## 5. Claim Creator Fees Creator fees on a non-SOL launch accrue in the pool's **quote mint** and are claimed from the fee-share position custody. The claim flow is: 1. `GET /token-launch/claimable-positions` to find the claimable position for your wallet. 2. `POST /token-launch/claim-txs/v2` with the position's `dammPositionInfo` to build the claim transaction. The simplified `sdk.fee.getClaimTransactions()` method (which calls `claim-txs/v3`) does **not** currently build custody claims for non-SOL DAMM v2 direct launches. Use the `claim-txs/v2` endpoint with the `dammPositionInfo` object returned by `claimable-positions`, as shown below. ```typescript theme={null} const BASE_URL = "https://public-api-v2.bags.fm/api/v1"; async function claimCreatorFees(tokenMint: string, keypair: Keypair) { const commitment = sdk.state.getCommitment(); // 1. Find the claimable DAMM v2 direct position for this wallet + token const positionsRes = await fetch( `${BASE_URL}/token-launch/claimable-positions?wallet=${keypair.publicKey.toBase58()}`, { headers: { "x-api-key": BAGS_API_KEY! } } ); const { response: positions } = await positionsRes.json(); const position = positions.find( (p: any) => p.launchType === "DAMM_V2_DIRECT" && p.baseMint === tokenMint ); if (!position) { console.log("No claimable fees for this token."); return; } console.log(`💰 Claimable: ${position.claimableDisplayAmount} (in quote token)`); // 2. Build claim transactions. The position's `dammPositionInfo` already contains every // field claim-txs/v2 needs, so post it directly as the request body. const claimRes = await fetch(`${BASE_URL}/token-launch/claim-txs/v2`, { method: "POST", headers: { "x-api-key": BAGS_API_KEY!, "content-type": "application/json", }, body: JSON.stringify(position.dammPositionInfo), }); const { response: claimTxs } = await claimRes.json(); // 3. Sign and send each returned transaction for (const { tx } of claimTxs) { const transaction = VersionedTransaction.deserialize(bs58.decode(tx)); const signature = await signAndSendTransaction(connection, commitment, transaction, keypair); console.log(`✅ Claim confirmed: ${signature}`); } } ``` Claimed amounts are paid out in the pool's quote mint, not SOL. Convert using the quote mint's decimals when displaying balances. ## 6. Claim Partner and Deployer Fees Partners and deployers do not hold a DAMM v2 position directly. Their revenue share accrues into a per-wallet **aggregate vault** (one per quote mint) that is swept with the vault endpoints. These claims are gas-sponsored: the gas sponsor pays the transaction fee and your wallet co-signs as the authorizer. ```typescript theme={null} async function sweepVaults(keypair: Keypair) { const commitment = sdk.state.getCommitment(); // 1. List every non-empty vault balance for this wallet const vaults = await sdk.tokenLaunch.getDammV2VaultClaimables(keypair.publicKey); if (vaults.length === 0) { console.log("No vault balances to claim."); return; } // 2. Sweep each vault for (const vault of vaults) { console.log(`💰 ${vault.kind} vault: ${vault.claimableDisplayAmount} (quote mint ${vault.quoteMint})`); const { transaction } = await sdk.tokenLaunch.claimDammV2Vault({ kind: vault.kind, // "partner" | "deployer" wallet: keypair.publicKey, quoteMint: new PublicKey(vault.quoteMint), }); // claimDammV2Vault returns a VersionedTransaction (already decoded) const signature = await signAndSendTransaction(connection, commitment, transaction, keypair); console.log(`✅ Vault swept: ${signature}`); } } ``` ## 7. Run Your Script Edit the `launchToken(...)` call at the bottom of the script with your token details and a `quoteMint` from step 2, then run: ```bash theme={null} npx ts-node launch-token-non-sol-quote.ts ``` ## 8. Troubleshooting Common issues: * **Token already launched**: The token mint must still be in `PRE_LAUNCH` status. Each mint from `createTokenInfoAndMetadata()` can only be launched once. * **Quote mint not supported**: Only mints returned by `getDammV2SupportedQuoteTokens()` can be used. Re-fetch the list, as it can change. * **Insufficient quote balance for initial buy**: `initialBuyQuoteAmount` is spent from your wallet's quote-token balance at build time. Make sure you hold at least that amount, in base units. * **Insufficient SOL**: Your wallet still needs SOL to pay transaction fees and rent, even though trading is quoted in a non-SOL token. * **Wrong decimals**: `initialBuyQuoteAmount`, claimable amounts, and vault balances are all in the quote mint's base units. Convert using the `decimals` from `getDammV2SupportedQuoteTokens()`. For more details, see the [API Reference](/api-reference/introduction). # Trade Tokens Source: https://docs.bags.fm/how-to-guides/trade-tokens Complete guide to get trade quotes and execute token swaps using the Bags API with TypeScript and Node.js In this guide, you'll learn how to get trade quotes and execute token swaps using the Bags TypeScript SDK with Node.js. The trade service allows you to swap tokens across various DEXs and liquidity pools on Solana. ## Prerequisites Before starting, make sure you have: * Completed our [TypeScript and Node.js Setup Guide](/how-to-guides/typescript-node-setup). * Got your API key from the [Bags Developer Portal](https://dev.bags.fm). * A Solana wallet with tokens to swap and SOL for transaction fees. * Installed the additional dependencies for this guide: ```bash theme={null} npm install @solana/web3.js bs58 ``` **Transaction Fees**: Trading tokens requires Solana transactions. Make sure your wallet has sufficient SOL balance to pay for transaction fees. ## 1. Set Up Environment Variables This guide requires your wallet's private key. Add it to your base `.env` file: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com PRIVATE_KEY=your_base58_encoded_private_key_here # Required for this guide ``` You can export your private key from wallets like Bags, Phantom, or Backpack. ## 2. The Token Trading Script Here is a complete script to get a trade quote and execute a swap. Save it as `trade-tokens.ts`. The script follows this flow: 1. Get a trade quote 2. Review the quote details 3. Create a swap transaction 4. Sign and send the transaction ## Endpoints Used Under the Hood This guide uses: * [`GET /trade/quote`](/api-reference/get-trade-quote) via `sdk.trade.getQuote()` * [`POST /trade/swap`](/api-reference/create-swap-transaction) via `sdk.trade.createSwapTransaction()` ```typescript theme={null} import dotenv from "dotenv"; dotenv.config({ quiet: true }); import { BagsSDK, signAndSendTransaction } from "@bagsfm/bags-sdk"; import { Keypair, PublicKey, Connection } from "@solana/web3.js"; import bs58 from "bs58"; // Initialize SDK const BAGS_API_KEY = process.env.BAGS_API_KEY; const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!BAGS_API_KEY || !SOLANA_RPC_URL || !PRIVATE_KEY) { throw new Error("BAGS_API_KEY, SOLANA_RPC_URL, and PRIVATE_KEY are required"); } const connection = new Connection(SOLANA_RPC_URL); const sdk = new BagsSDK(BAGS_API_KEY, connection, "processed"); async function executeSwap( inputMint: PublicKey, outputMint: PublicKey, amount: number, slippageMode: "auto" | "manual" = "auto", slippageBps?: number ) { try { if (!PRIVATE_KEY) { throw new Error("PRIVATE_KEY is not set"); } const keypair = Keypair.fromSecretKey(bs58.decode(PRIVATE_KEY)); const commitment = sdk.state.getCommitment(); console.log(`💱 Getting trade quote...`); console.log(` Input: ${inputMint.toBase58()}`); console.log(` Output: ${outputMint.toBase58()}`); console.log(` Amount: ${amount}`); console.log(` Slippage Mode: ${slippageMode}`); // Step 1: Get a trade quote const quote = await sdk.trade.getQuote({ inputMint: inputMint, outputMint: outputMint, amount: amount, slippageMode: slippageMode, slippageBps: slippageBps, }); console.log("\n📊 Quote Details:"); console.log(` Request ID: ${quote.requestId}`); console.log(` Input Amount: ${quote.inAmount}`); console.log(` Output Amount: ${quote.outAmount}`); console.log(` Min Output Amount: ${quote.minOutAmount}`); console.log(` Price Impact: ${quote.priceImpactPct}%`); console.log(` Slippage: ${quote.slippageBps / 100}%`); console.log(` Route Plan: ${quote.routePlan.length} leg(s)`); // Display route plan details if (quote.routePlan.length > 0) { console.log("\n🛣️ Route Plan:"); quote.routePlan.forEach((leg, index) => { console.log(` Leg ${index + 1}:`); console.log(` Venue: ${leg.venue}`); console.log(` Input: ${leg.inAmount} (${leg.inputMint})`); console.log(` Output: ${leg.outAmount} (${leg.outputMint})`); }); } // Display platform fee if present if (quote.platformFee) { console.log("\n💰 Platform Fee:"); console.log(` Amount: ${quote.platformFee.amount}`); console.log(` Fee BPS: ${quote.platformFee.feeBps}`); console.log(` Fee Account: ${quote.platformFee.feeAccount}`); } // Step 2: Create swap transaction console.log("\n🎯 Creating swap transaction..."); const swapResult = await sdk.trade.createSwapTransaction({ quoteResponse: quote, userPublicKey: keypair.publicKey, }); console.log(` Compute Unit Limit: ${swapResult.computeUnitLimit}`); console.log(` Prioritization Fee: ${swapResult.prioritizationFeeLamports} lamports`); // Step 3: Sign and send transaction console.log("\n🔑 Signing and sending swap transaction..."); const signature = await signAndSendTransaction(connection, commitment, swapResult.transaction, keypair); console.log("\n🎉 Swap executed successfully!"); console.log(` Transaction Signature: ${signature}`); console.log(` View on Solana Explorer: https://solscan.io/tx/${signature}`); return { signature, quote, swapResult, }; } catch (error) { console.error("🚨 Swap execution failed:", error); throw error; } } // Example: Swap 100,000 tokens (adjust decimals based on token) // Replace with your actual token mint addresses const INPUT_MINT = new PublicKey("YOUR_INPUT_TOKEN_MINT_ADDRESS"); const OUTPUT_MINT = new PublicKey("YOUR_OUTPUT_TOKEN_MINT_ADDRESS"); const AMOUNT = 100_000; // Amount in token's smallest unit (e.g., if token has 6 decimals, 100000 = 0.1 tokens) // Execute swap with auto slippage executeSwap(INPUT_MINT, OUTPUT_MINT, AMOUNT, "auto") .then((result) => { console.log("\n✨ Swap completed successfully!"); }) .catch((error) => { console.error("🚨 Unexpected error occurred:", error); }); // Example: Execute swap with manual slippage (1% = 100 bps) // executeSwap(INPUT_MINT, OUTPUT_MINT, AMOUNT, "manual", 100) // .then((result) => { // console.log("\n✨ Swap completed successfully!"); // }) // .catch((error) => { // console.error("🚨 Unexpected error occurred:", error); // }); ``` ## 3. Understanding Trade Quotes A trade quote provides information about a potential swap before you execute it: ### Quote Parameters * **inputMint**: The token you want to swap from (PublicKey) * **outputMint**: The token you want to swap to (PublicKey) * **amount**: The amount to swap (in the token's smallest unit, e.g., lamports for SOL) * **slippageMode**: Either `"auto"` (automatic slippage calculation) or `"manual"` (you specify slippage) * **slippageBps**: Basis points for slippage tolerance (0-10000, where 10000 = 100%). Required when `slippageMode` is `"manual"` ### Quote Response The quote response includes: * **inAmount**: The input amount (as string) * **outAmount**: The expected output amount (as string) * **minOutAmount**: The minimum output amount considering slippage * **priceImpactPct**: The price impact percentage (as string) * **slippageBps**: The slippage tolerance in basis points * **routePlan**: Array of route legs showing the swap path through different venues * **platformFee**: Optional platform fee information * **requestId**: Unique identifier for the quote request ### Route Plan The route plan shows how your swap will be executed across different venues (DEXs, liquidity pools, etc.). Each leg represents one step in the swap path. ## 4. Slippage Modes ### Auto Slippage When using `slippageMode: "auto"`, the SDK automatically calculates an appropriate slippage tolerance based on market conditions. This is recommended for most use cases. ```typescript theme={null} const quote = await sdk.trade.getQuote({ inputMint: inputMint, outputMint: outputMint, amount: amount, slippageMode: "auto", }); ``` ### Manual Slippage When using `slippageMode: "manual"`, you must specify `slippageBps`. This gives you full control over slippage tolerance. ```typescript theme={null} const quote = await sdk.trade.getQuote({ inputMint: inputMint, outputMint: outputMint, amount: amount, slippageMode: "manual", slippageBps: 100, // 1% slippage tolerance }); ``` **Slippage BPS Examples:** * `50` = 0.5% slippage tolerance * `100` = 1% slippage tolerance * `500` = 5% slippage tolerance * `1000` = 10% slippage tolerance ## 5. Swap Transaction Details When you create a swap transaction, you receive: * **transaction**: A `VersionedTransaction` ready to be signed and sent * **computeUnitLimit**: The compute unit limit for the transaction * **lastValidBlockHeight**: The last valid block height for the transaction * **prioritizationFeeLamports**: The prioritization fee in lamports The transaction is already configured with compute units and prioritization fees, so you can sign and send it directly. ## 6. Running the Script To execute a swap, edit the script with your token mint addresses and amount: 1. Set `INPUT_MINT` to the token you want to swap from 2. Set `OUTPUT_MINT` to the token you want to swap to 3. Set `AMOUNT` to the amount in the token's smallest unit (consider token decimals) Then, run the script from your terminal: ```bash theme={null} npx ts-node trade-tokens.ts ``` ## 7. Getting a Quote Only If you just want to check a quote without executing a swap: ```typescript theme={null} async function getQuoteOnly(inputMint: PublicKey, outputMint: PublicKey, amount: number) { const quote = await sdk.trade.getQuote({ inputMint: inputMint, outputMint: outputMint, amount: amount, slippageMode: "auto", }); console.log(`Expected output: ${quote.outAmount}`); console.log(`Min output (with slippage): ${quote.minOutAmount}`); console.log(`Price impact: ${quote.priceImpactPct}%`); return quote; } ``` ## Alternative: Using the Bags CLI This section requires the Bags CLI. See [Install and Set Up the Bags CLI](/cli/install-and-setup) to get started. You can get quotes and execute swaps directly from the terminal without writing any code: **Get a quote:** ```bash theme={null} bags trade quote \ --input-mint So11111111111111111111111111111111 \ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --amount 1000000000 ``` **Execute a swap:** ```bash theme={null} bags trade swap \ --input-mint So11111111111111111111111111111111 \ --output-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \ --amount 1000000000 \ --skip-confirm ``` **With manual slippage (1%):** ```bash theme={null} bags trade swap \ --input-mint INPUT_MINT \ --output-mint OUTPUT_MINT \ --amount 1000000000 \ --slippage-mode manual \ --slippage-bps 100 ``` If you omit flags, the CLI prompts for each value interactively. Use `--json` to get machine-readable output for scripting. ## 8. Error Handling The script includes comprehensive error handling for: * **Invalid Token Mints**: Ensure the mint addresses are valid Solana public keys * **Insufficient Liquidity**: The quote may fail if there's not enough liquidity for the swap * **Invalid Amount**: The amount must be a positive number * **Slippage Errors**: If using manual slippage, ensure `slippageBps` is between 0 and 10000 * **Transaction Failures**: Network issues or insufficient SOL for fees ## 9. Troubleshooting Common issues include: * **No Quote Available**: Check that both tokens have sufficient liquidity and are tradeable * **High Price Impact**: Large swaps may have high price impact. Consider splitting into smaller swaps * **Insufficient SOL**: Your wallet needs SOL for transaction fees * **Invalid Amount**: Make sure the amount is in the token's smallest unit (not in human-readable format) For more details, see the [API Reference](/api-reference/introduction). # Set Up a TypeScript & Node.js Project Source: https://docs.bags.fm/how-to-guides/typescript-node-setup Follow these steps to set up a TypeScript and Node.js project for interacting with the Bags API. Before you can follow our TypeScript and Node.js how-to guides, you need to set up your development environment. This guide will walk you through creating a new project, installing essential packages, and configuring your environment. ## 1. Initialize Your TypeScript Project First, create a new directory for your project and initialize it: ```bash theme={null} mkdir my-bags-project cd my-bags-project npm init -y ``` Next, install TypeScript and initialize the TypeScript configuration: ```bash theme={null} npm install -g typescript npx tsc --init ``` This will create a `tsconfig.json` file with default settings, which you can customize as needed. ## 2. Install Core Dependencies Now, install the core dependencies required for most interactions with the Bags API and for running your TypeScript code with Node.js. ```bash theme={null} npm install @bagsfm/bags-sdk dotenv @solana/web3.js bs58 ``` Also, install the necessary development dependencies: ```bash theme={null} npm install -D typescript ts-node @types/node ``` ## 3. Set Up Environment Variables Create a `.env` file in your project root to store your Bags API key, Solana RPC URL, and other secrets: ```bash theme={null} # .env BAGS_API_KEY=your_api_key_here SOLANA_RPC_URL=https://api.mainnet-beta.solana.com # PRIVATE_KEY=your_base58_encoded_private_key_here # Required for guides that perform transactions ``` You can get your API key from [dev.bags.fm](https://dev.bags.fm). Some guides might require additional variables like your wallet's private key for signing transactions. Never commit your `.env` file to version control. Add it to your `.gitignore` file to keep your secrets safe. ## 4. Project Structure Your project structure should look like this: ``` my-bags-project/ ├── package.json ├── tsconfig.json ├── .env ├── .gitignore └── src/ └── (your TypeScript files will go here) ``` Make sure to add `.env` to your `.gitignore` file: ```bash theme={null} # .gitignore .env node_modules/ dist/ ``` You are now ready to follow our how-to guides! Each guide will include the complete setup code needed to initialize the SDK and run the examples. # Bags API Documentation Source: https://docs.bags.fm/index Build powerful applications with the Bags API - get started with authentication, rate limits, and your first API call ## Getting started The Bags API allows you to integrate Bags functionality into your applications. Get up and running in minutes. ## Authentication All API requests require authentication using an API key. ### Get your API key 1. Visit [dev.bags.fm](https://dev.bags.fm) and sign in to your account 2. Navigate to the API Keys section 3. Create a new API key Each user can create up to 10 API keys. Keep your keys secure and never share them publicly. ### Using your API key Include your API key in the `x-api-key` header with every request: ```bash cURL theme={null} curl -X GET 'https://public-api-v2.bags.fm/api/v1/endpoint' \ -H 'x-api-key: YOUR_API_KEY' ``` ```javascript Node.js theme={null} const response = await fetch('https://public-api-v2.bags.fm/api/v1/endpoint', { headers: { 'x-api-key': 'YOUR_API_KEY' } }); ``` ```python Python theme={null} import requests response = requests.get('https://public-api-v2.bags.fm/api/v1/endpoint', headers={'x-api-key': 'YOUR_API_KEY'}) ``` ### Managing API keys You can revoke API keys at any time: 1. Go to [dev.bags.fm](https://dev.bags.fm) 2. Find the key you want to revoke 3. Click "Revoke" to permanently disable it Revoking an API key immediately stops all requests using that key. Update your applications before revoking keys that are in use. ## Rate limits The Bags API implements rate limiting to ensure fair usage and system stability. * **Rate limit**: 5,000 requests per hour per user and per ip * **Scope**: Rate limits apply across all your API keys * **Headers**: Check `X-RateLimit-Remaining` and `X-RateLimit-Reset` in response headers Distribute requests evenly throughout the hour to avoid hitting rate limits. Consider implementing exponential backoff for failed requests. ## Core Principles Get familiar with key concepts and best practices: API endpoint structure and versioning information. Understanding API error responses and status codes. Monitor usage and avoid hitting rate limits. Upload images and files for token creation. Best practices for securing and organizing API keys. Complete guide to creating Solana tokens. ## Explore the API Complete endpoint documentation with examples. Install and use the Bags CLI for quick terminal-based workflows. # API Key Management Source: https://docs.bags.fm/principles/api-key-management Best practices for creating, organizing, and securing your API keys Manage your API keys efficiently to maintain security and organization across your applications. ## Getting Your First API Key 1. Visit [dev.bags.fm](https://dev.bags.fm) and sign in to your account 2. Navigate to the API Keys section 3. Create a new API key with a descriptive name 4. Copy and securely store your API key Each user can create up to 10 API keys. Keep your keys secure and never share them publicly. ## Key Limitations * **Maximum keys**: 10 API keys per user account * **Key naming**: Name your keys for easy organization and identification * **Usage tracking**: Keys track last usage timestamp for monitoring * **Immediate revocation**: Revoking a key instantly stops all requests using that key ## Best Practices ### 1. Descriptive Naming Use clear, descriptive names that identify the key's purpose: **Good examples:** * `Production-Web-App` * `Development-Environment` * `Mobile-App-iOS` * `Background-Jobs-Server` * `Testing-Integration` **Poor examples:** * `Key1` * `Test` * `MyKey` * `temp` ### 2. Environment Separation Create separate keys for different environments: ```javascript theme={null} // Environment-specific configurations const config = { development: { apiKey: 'bags_test_dev123...', baseURL: 'https://public-api-v2.bags.fm/api/v1/' }, staging: { apiKey: 'bags_test_staging456...', baseURL: 'https://public-api-v2.bags.fm/api/v1/' }, production: { apiKey: 'bags_live_prod789...', baseURL: 'https://public-api-v2.bags.fm/api/v1/' } }; ``` ### 3. Application-Specific Keys Use different keys for different applications or services: | Application | Key Name | Purpose | | ----------------- | -------------------- | -------------------- | | Web Dashboard | `Web-Dashboard-Prod` | Main web application | | Mobile App | `Mobile-App-v2` | iOS/Android app | | Background Jobs | `Cronjobs-Server` | Scheduled tasks | | Analytics | `Analytics-Service` | Data collection | | Integration Tests | `CI-CD-Testing` | Automated testing | ## Emergency Procedures ### Compromised Key Response If an API key is compromised: 1. **Immediately revoke** the compromised key 2. **Create a new key** with different name 3. **Update all applications** using the old key 4. **Review logs** for unauthorized usage 5. **Report the incident** if further assistance is required Always update your applications with new API keys before revoking old ones to prevent service interruptions. # Base URL & Versioning Source: https://docs.bags.fm/principles/base-url-versioning API endpoint structure and versioning information The Bags API uses a consistent base URL structure for all endpoints: * **Base URL**: `https://public-api-v2.bags.fm/api/v1/` * **Current version**: v1 * **Health check**: GET `/ping` returns `{message: "pong"}` All endpoints are prefixed with the base URL. Future API versions will be released with updated version numbers in the path. ## Health Check Test API connectivity: ```bash cURL theme={null} curl https://public-api-v2.bags.fm/ping ``` ```javascript Node.js theme={null} const response = await fetch('https://public-api-v2.bags.fm/ping'); const data = await response.json(); console.log(data); // { message: "pong" } ``` ```python Python theme={null} import requests response = requests.get('https://public-api-v2.bags.fm/ping') print(response.json()) # { "message": "pong" } ``` **Response:** ```json theme={null} { "message": "pong" } ``` ## Version History | Version | Release Date | Status | Breaking Changes | | ------- | ------------ | ------- | --------------------- | | v1 | 2025-08-02 | Current | N/A (Initial release) | Future API versions will maintain backward compatibility where possible. Breaking changes will be clearly documented and communicated in advance. # Error Handling Source: https://docs.bags.fm/principles/error-handling Understanding API error responses and status codes The Bags API uses standardized error responses to help you handle issues in your applications. ## Error Response Format All errors return JSON with a consistent structure: ```json theme={null} { "success": false, "error": "Detailed error message" } ``` Success responses use a different format: ```json theme={null} { "success": true, "response": { // Response data here } } ``` ## Common Status Codes | Status Code | Description | When It Occurs | | ----------- | --------------------- | ------------------------------------------------- | | 400 | Bad Request | Invalid request parameters or validation errors | | 401 | Unauthorized | Missing or invalid API key/authentication | | 403 | Forbidden | Valid authentication but insufficient permissions | | 404 | Not Found | Resource not found | | 413 | Payload Too Large | File upload exceeds size limit | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Unexpected server error | ## Error Examples ### Validation Error (400) ```json theme={null} { "success": false, "error": "Token name is required and must be between 1-32 characters" } ``` ### Authentication Error (401) ```json theme={null} { "success": false, "error": "Invalid API key. Please check your x-api-key header." } ``` ### Permission Error (403) ```json theme={null} { "success": false, "error": "API key does not have permission to access this resource" } ``` ### Rate Limit Error (429) When you exceed rate limits, the API returns additional information: ```json theme={null} { "success": false, "error": "Rate limit exceeded", "limit": 5000, "remaining": 0, "resetTime": 1672531200 } ``` ### File Upload Error (413) ```json theme={null} { "success": false, "error": "Image file must be under 15MB" } ``` ### Server Error (500) ```json theme={null} { "success": false, "error": "An unexpected error occurred. Please try again later." } ``` ## SDK Error Handling When using the Bags TypeScript SDK, errors are automatically handled and thrown as exceptions. The SDK wraps API responses and throws errors for failed requests: ```typescript theme={null} import { BagsSDK } from "@bagsfm/bags-sdk"; try { const sdk = new BagsSDK(apiKey, connection); const result = await sdk.tokenLaunch.createTokenInfoAndMetadata({...}); // Success - result contains the response data directly } catch (error) { // The SDK automatically throws errors for API failures // Error messages contain details about what went wrong console.error('Error:', error.message); // You can check error properties if available if (error.status) { console.error('Status:', error.status); } } ``` The SDK automatically handles the `success: false` responses and throws errors, so you don't need to manually check the `success` field. Successful responses return the data directly from the `response` field. ## Best Practices ### Error Handling in Code ```javascript Node.js theme={null} try { const response = await fetch('https://public-api-v2.bags.fm/api/v1/endpoint', { headers: { 'x-api-key': 'YOUR_API_KEY' } }); const data = await response.json(); if (!data.success) { console.error('API Error:', data.error); // Handle specific error cases switch (response.status) { case 401: // Redirect to login or refresh API key break; case 429: // Implement exponential backoff break; default: // Generic error handling } return; } // Process successful response console.log(data.response); } catch (error) { console.error('Network error:', error); } ``` ```python Python theme={null} import requests import time def handle_api_request(endpoint, headers): try: response = requests.get(endpoint, headers=headers) data = response.json() if not data.get('success', False): print(f"API Error: {data.get('error')}") if response.status_code == 401: # Handle authentication error pass elif response.status_code == 429: # Implement retry with backoff time.sleep(60) # Wait 1 minute return handle_api_request(endpoint, headers) return None return data['response'] except requests.RequestException as e: print(f"Network error: {e}") return None ``` ### Retry Logic Implement exponential backoff for rate limit and server errors: 1. **429 (Rate Limited)**: Wait based on `resetTime` or implement exponential backoff 2. **500/502/503**: Retry with exponential backoff (max 5 attempts) 3. **400/401/403/404**: Don't retry - fix the request first Check the `X-RateLimit-*` headers to proactively avoid rate limits rather than handling them reactively. # File Upload Support Source: https://docs.bags.fm/principles/file-uploads How to upload images and files through the Bags API The Bags API supports file uploads for token creation with specific requirements and formats. ## Upload Requirements * **Maximum file size**: 15MB for image uploads * **Content-Type**: `multipart/form-data` for file upload endpoints * **File field name**: `image` (required field name for all image uploads) * **Supported formats**: PNG, JPG, JPEG, GIF, WebP ## Basic File Upload ### Single Image Upload ```bash cURL theme={null} curl -X POST 'https://public-api-v2.bags.fm/api/v1/token-launch/create-token-info' \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: multipart/form-data' \ -F 'image=@/path/to/your/image.png' \ -F 'name=My Token' \ -F 'symbol=MYTOKEN' ``` ```javascript Node.js theme={null} // From file input const formData = new FormData(); formData.append('image', fileInput.files[0]); formData.append('name', 'My Token'); formData.append('symbol', 'MYTOKEN'); const response = await fetch('https://public-api-v2.bags.fm/api/v1/token-launch/create-token-info', { method: 'POST', headers: { 'x-api-key': 'YOUR_API_KEY' }, body: formData }); // From File object const file = new File([imageBlob], 'token-image.png', { type: 'image/png' }); formData.append('image', file); ``` ```python Python theme={null} import requests # From file path files = {'image': open('/path/to/your/image.png', 'rb')} data = { 'name': 'My Token', 'symbol': 'MYTOKEN' } response = requests.post( 'https://public-api-v2.bags.fm/api/v1/token-launch/create-token-info', headers={'x-api-key': 'YOUR_API_KEY'}, files=files, data=data ) # Don't forget to close the file files['image'].close() # Or use context manager with open('/path/to/your/image.png', 'rb') as f: files = {'image': f} response = requests.post(url, headers=headers, files=files, data=data) ``` ## Advanced Upload Examples ### Validate File Before Upload ```javascript Node.js theme={null} function validateImageFile(file) { // Check file size (15MB = 15 * 1024 * 1024 bytes) const maxSize = 15 * 1024 * 1024; if (file.size > maxSize) { throw new Error('File size must be under 15MB'); } // Check file type const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { throw new Error('File must be PNG, JPG, JPEG, GIF, or WebP'); } return true; } // Usage try { validateImageFile(fileInput.files[0]); // Proceed with upload } catch (error) { console.error('Validation error:', error.message); } ``` ```python Python theme={null} import os def validate_image_file(file_path): # Check file size (15MB) max_size = 15 * 1024 * 1024 if os.path.getsize(file_path) > max_size: raise ValueError('File size must be under 15MB') # Check file extension allowed_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.webp'} file_ext = os.path.splitext(file_path)[1].lower() if file_ext not in allowed_extensions: raise ValueError('File must be PNG, JPG, JPEG, GIF, or WebP') return True # Usage try: validate_image_file('/path/to/image.png') # Proceed with upload except ValueError as e: print(f'Validation error: {e}') ``` ## Error Handling ### Common Upload Errors **File too large (413):** ```json theme={null} { "success": false, "error": "Image file must be under 15MB" } ``` **Invalid file type (400):** ```json theme={null} { "success": false, "error": "Unsupported file type. Please upload PNG, JPG, JPEG, GIF, or WebP images." } ``` **Missing file (400):** ```json theme={null} { "success": false, "error": "Image file is required" } ``` **Corrupted file (400):** ```json theme={null} { "success": false, "error": "Invalid image file. Please check your file and try again." } ``` ## Best Practices 1. **Always validate files client-side** before uploading to save bandwidth and API quota 2. **Compress images** when possible to improve upload speed and user experience 3. **Show progress indicators** for better user experience during uploads 4. **Handle network interruptions** with retry logic for failed uploads 5. **Use appropriate image formats** - PNG for graphics with transparency, JPEG for photos 6. **Optimize image dimensions** - resize to appropriate dimensions before upload Uploaded images are processed and optimized by the Bags system. The final image URL may differ from your uploaded version. Consider implementing client-side image compression to improve upload speeds and stay within file size limits. # Address Lookup Tables (LUTs) Source: https://docs.bags.fm/principles/lookup-tables Public LUTs maintained by Bags and how to use them ### Overview Solana Address Lookup Tables (LUTs) let you reference many accounts in a transaction without having to include their full 32-byte public keys directly. This reduces transaction size and helps you fit more instructions per transaction. Bags maintains a public LUT that contains the most commonly used accounts across our APIs and products. You can use it for all Bags-related transactions. * Mainnet-beta LUT address: `Eq1EVs15EAWww1YtPTtWPzJRLPJoS6VYP9oW9SbNr3yp` The LUT is updated over time as our products evolve. If you have suggestions for accounts or providers to include, reach out to us. ### What's inside * Core program IDs and frequently used accounts for Bags workflows * Accounts related to token launches, fee sharing/claiming, and post-launch liquidity * Well-known tipping provider recipient wallets (see below) You can inspect the table contents via your RPC or CLI to confirm which accounts are currently included. ### Using the LUT in transactions * Include the LUT address when compiling or building your transaction message so that account keys are resolved from the table at runtime. * The transaction fee payer must have access to the LUT on mainnet-beta; no special permissions are required to read from a public LUT. * LUT usage is optional for most transactions. Your transactions will still work without it, but may be larger. ### When LUTs are Required For fee share configurations, LUTs are **required** when you have more than 15 fee claimers. The SDK provides a helper function `getConfigCreationLookupTableTransactions()` to create the necessary LUT transactions: 1. Create the LUT creation transaction 2. Execute the creation transaction 3. Wait for one slot to pass (required by Solana) 4. Execute all LUT extend transactions 5. Pass the LUT addresses to `createBagsFeeShareConfig` via `additionalLookupTables` See the [Launch a Token](/how-to-guides/launch-token) guide for a complete example of LUT creation and usage. ### Recommended tip recipients (from the Bags LUT) While you can tip any valid Solana address, we recommend using a provider wallet that is already present in our LUT for convenience and compact transactions. Current provider recipients included in the LUT: * Jito: * `96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5` * `HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe` * `Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY` * `ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49` * `DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh` * `ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt` * `DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL` * `3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT` * bloXroute: * `HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY` * `95cfoy472fcQHaw4tPGBTKpn6ZQnfEPfBgDQx6gcRmRg` * `3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd` * `FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF` * Astral: * `astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF` * `astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm` * `astra9xWY93QyfG6yM8zwsKsRodscjQ2uU2HKNL5prk` * `astraRVUuTHjpwEVvNBeQEgwYx9w9CFyfxjYoobCZhL` * `astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK` * `astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC` * `astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk` * `astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK` See the tipping guide for how to include a tip in Bags-generated transactions. Using a recipient already in the LUT keeps transactions as compact as possible. ### Propose additions If you operate a relevant service or have account suggestions that should be in our LUT, please reach out. We're happy to review and expand coverage where it benefits developers. # Program IDs Source: https://docs.bags.fm/principles/program-ids Solana programs used by Bags and where to find their IDLs This page lists the Solana programs actively used by Bags and shows where to find their Interface Definition Language (IDL) files. All program IDs below refer to mainnet-beta deployments unless otherwise noted. ## Active Programs | Program | Purpose | Program ID | | ----------------- | --------------------------------------------------------------- | ---------------------------------------------- | | Bags Fee Share V1 | Custom fee splits and fee claiming (legacy) | `FEEhPbKVKnco9EXnaY3i4R5rQVUx91wgVfu8qokixywi` | | Bags Fee Share V2 | Fee share configuration and claiming (current) | `FEE2tBhCKAt7shrod19QttSVREUYPiyMzoku1mL1gqVK` | | Meteora DAMM v2 | Post-migration tokens (AMM/pool after bonding curve graduation) | `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` | | Meteora DBC | Token creation and bonding curve management | `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` | ## Program Overviews ### Bags Fee Share V1 (Legacy) * **Purpose**: Legacy fee share program for custom fee splits and fee claiming. * **Program ID**: `FEEhPbKVKnco9EXnaY3i4R5rQVUx91wgVfu8qokixywi` * **Status**: Still supported for backward compatibility with existing positions ### Bags Fee Share V2 (Current) * **Purpose**: Current fee share program for configuring fee splits and claiming fees. Used by Token Launch v2. * **Program ID**: `FEE2tBhCKAt7shrod19QttSVREUYPiyMzoku1mL1gqVK` * **Features**: Supports multiple fee claimers, partner configurations, and lookup tables for large configurations ### Meteora DAMM v2 * **Purpose**: AMM used by tokens after migrating from bonding curves; supports SPL and Token-2022 flows. * **Program ID**: `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG` * **More info**: See the Meteora developer docs. ### Meteora DBC * **Purpose**: Token creation and bonding curve lifecycle management prior to AMM migration. * **Program ID**: `dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN` * **More info**: See the Meteora developer docs. ## IDLs * **All actively used IDLs (including Meteora and Bags)** are available in the Bags SDK repository: * `bags-sdk` IDLs: `https://github.com/bagsfm/bags-sdk/tree/main/src/idl` You can consume these IDLs directly in your build tooling or download them for offline use when generating clients. ## Address Lookup Table * To save on transaction size when interacting with Bags programs and instructions, you can use our public Address Lookup Table (LUT). * **LUT address**: `Eq1EVs15EAWww1YtPTtWPzJRLPJoS6VYP9oW9SbNr3yp` * It includes the most commonly used accounts and will be extended over time. ## References * Meteora Developer Guide: `https://docs.meteora.ag/developer-guide/home` * Bags SDK IDLs: `https://github.com/bagsfm/bags-sdk/tree/main/src/idl` ## Verification Checklist * **Network**: Ensure you are on mainnet-beta when interacting with the above program IDs. # Rate Limits Source: https://docs.bags.fm/principles/rate-limits Understanding API rate limits and monitoring usage The Bags API implements rate limiting to ensure fair usage and system stability. ## Rate Limit Overview * **Rate limit**: 5,000 requests per hour per user and per ip * **Scope**: Rate limits apply across all your API keys * **System**: Sliding hourly windows (not fixed periods) ## Endpoint-Specific Limits Certain endpoints may enforce additional, endpoint-specific rate limits. These limits are intentionally not publicly disclosed and apply only to the affected endpoints. Under normal usage patterns you should not encounter these limits. If you do get rate limited, please reach out to us so we can help. ## Response Headers Monitor your API usage through response headers included with every request: * **`X-RateLimit-Limit`**: Total requests allowed per hour (5,000) * **`X-RateLimit-Remaining`**: Requests remaining in current window * **`X-RateLimit-Reset`**: Unix timestamp when the limit resets ## Sliding Window System Rate limits use sliding hourly windows rather than fixed periods: * Window 1: 1:00 PM - 2:00 PM * Window 2: 2:00 PM - 3:00 PM * And so on... This means your rate limit resets continuously rather than at fixed times. ## Monitoring Usage ```bash cURL theme={null} curl -I -X GET 'https://public-api-v2.bags.fm/ping' \ -H 'x-api-key: YOUR_API_KEY' # Response headers include: # X-RateLimit-Limit: 5000 # X-RateLimit-Remaining: 4999 # X-RateLimit-Reset: 1672531200 ``` ```javascript Node.js theme={null} const response = await fetch('https://public-api-v2.bags.fm/ping', { headers: { 'x-api-key': 'YOUR_API_KEY' } }); console.log('Rate Limit:', response.headers.get('X-RateLimit-Limit')); console.log('Remaining:', response.headers.get('X-RateLimit-Remaining')); console.log('Reset Time:', response.headers.get('X-RateLimit-Reset')); // Convert reset time to readable format const resetTime = new Date(parseInt(response.headers.get('X-RateLimit-Reset')) * 1000); console.log('Resets at:', resetTime.toLocaleString()); ``` ```python Python theme={null} import requests from datetime import datetime response = requests.get( 'https://public-api-v2.bags.fm/ping', headers={'x-api-key': 'YOUR_API_KEY'} ) print(f"Rate Limit: {response.headers.get('X-RateLimit-Limit')}") print(f"Remaining: {response.headers.get('X-RateLimit-Remaining')}") print(f"Reset Time: {response.headers.get('X-RateLimit-Reset')}") # Convert reset time to readable format reset_timestamp = int(response.headers.get('X-RateLimit-Reset')) reset_time = datetime.fromtimestamp(reset_timestamp) print(f"Resets at: {reset_time}") ``` ## Rate Limit Exceeded When you exceed your rate limit, the API returns a `429` status with additional information: ```json theme={null} { "success": false, "error": "Rate limit exceeded", "limit": 5000, "remaining": 0, "resetTime": 1672531200 } ``` ## Rate Limit Planning ### Calculate Request Budget Plan your API usage based on your application's needs: * **5,000 requests/hour** = \~83.3 requests/minute = \~1.39 requests/second * **High-frequency apps**: Consider request batching or caching * **Background jobs**: Spread requests across the hour Rate limits apply across all API keys for your account. Creating multiple API keys does not increase your rate limit. If you need to increase your rate limit, please contact us. # Priority Fees and Tips Source: https://docs.bags.fm/principles/tipping How to optionally add tips to supported endpoints ### Overview Some endpoints support adding an optional tip to a wallet of your choice. This lets you direct a portion of lamports to any provider (e.g., Jito, Astral) or a wallet you control when constructing transactions. By default, all transactions generated by these endpoints already include appropriate Compute Budget instructions: * `setComputeUnitLimit` and `setComputeUnitPrice` are set to optimize for reliable inclusion and cost-efficiency. * No tip is included by default. Tips are only added when you provide `tipWallet` and `tipLamports`. ### Supported endpoints * `POST /token-launch/create-launch-transaction` * `POST /fee-share/config` (fee share configuration creation) These endpoints accept two optional fields in the request body: * `tipWallet` (string): Base58 encoded Solana public key of the tip recipient wallet. * `tipLamports` (number): Tip amount in lamports. If omitted, no tip is included. ### How it works When you include `tipWallet` and `tipLamports`, the API appends a tip transfer as the final instruction in the generated transaction(s). Priority fee settings (`setComputeUnitLimit` and `setComputeUnitPrice`) are always included regardless of tipping. You are responsible for: * Submitting and confirming the returned transaction(s) * Ensuring the provided `tipWallet` is valid and owned by the intended provider * Choosing an appropriate `tipLamports` value Notes: * Tipping is purely optional and does not affect endpoint functionality when omitted. * There is no provider/key allowlist. You can use any valid Base58 encoded Solana public key for `tipWallet`. * The transaction fee payer funds both the network fees and the `tipLamports` transfer. Ensure the payer has sufficient SOL for all costs. ### Example payloads Add a tip when creating a token launch transaction: ```json theme={null} { "ipfs": "https://ipfs.io/ipfs/...", "tokenMint": "...", "wallet": "...", "initialBuyLamports": 25000000, "configKey": "...", "tipWallet": "JitoOrAstralWalletBase58...", "tipLamports": 100000 } ``` Add a tip when creating a fee-share config: ```json theme={null} { "payer": "...", "baseMint": "...", "feeClaimers": [ { "user": "...", "userBps": 5000 }, { "user": "...", "userBps": 5000 } ], "tipWallet": "TipRecipientBase58...", "tipLamports": 100000 } ``` ### Recommended providers You can use any valid Base58 Solana address for `tipWallet`. For compact transactions, we recommend using a recipient that is already included in our public Address Lookup Table (LUT): `Eq1EVs15EAWww1YtPTtWPzJRLPJoS6VYP9oW9SbNr3yp`. See [Address Lookup Tables](/principles/lookup-tables) for details. Current provider recipients included in the LUT: * Jito: * `96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5` * `HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe` * `Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY` * `ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49` * `DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh` * `ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt` * `DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL` * `3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT` * bloXroute: * `HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY` * `95cfoy472fcQHaw4tPGBTKpn6ZQnfEPfBgDQx6gcRmRg` * `3UQUKjhMKaY2S6bjcQD6yHB7utcZt5bfarRCmctpRtUd` * `FogxVNs6Mm2w9rnGL1vkARSwJxvLE8mujTv3LK8RnUhF` * Astral: * `astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF` * `astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm` * `astra9xWY93QyfG6yM8zwsKsRodscjQ2uU2HKNL5prk` * `astraRVUuTHjpwEVvNBeQEgwYx9w9CFyfxjYoobCZhL` * `astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK` * `astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC` * `astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk` * `astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK` If you have additional providers to recommend for inclusion in the LUT, reach out to us. ### Validation and limits * `tipWallet` must be a valid Base58 encoded Solana public key (any key is allowed; no allowlist) * `tipLamports` must be a positive integer within your balance constraints * The API does not currently enforce provider allowlists; use caution and verify recipients ### Troubleshooting * If a transaction fails to simulate or send, verify your `tipWallet`, `tipLamports`, and that your payer has sufficient SOL. * If you do not see the tip reflected on-chain, confirm the final submitted transaction includes the tip instruction and was confirmed. # Claim Creator Fees Source: https://docs.bags.fm/robinhood/claim-fees Understand the Bags fee-share model on Robinhood Chain and claim accrued creator fees from a token's BagsFeeShare contract with viem. In this guide, you'll read and claim trading fees on Robinhood Chain. Each token has its own `BagsFeeShare` contract that accrues the creator half of the trade fee (**1% of every trade**, in WETH) for the claimers configured at launch, plus the optional partner's cut of the protocol half. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup). * A token whose `feeShare` address you can resolve (from launch, `factory.feeShareForToken(token)`, or `BagsLens.getTokenState`). * A wallet with a positive claimable balance — i.e. one of the token's configured fee claimers or the optional partner. Check with `claimableOf(token, user)` (or `feeShare.claimable(user)`). To use the Bags API instead of reading contracts directly, call [Get Robinhood Claimable Positions](/api-reference/get-rh-claimable-positions), then [Create Robinhood Claim Transactions](/api-reference/create-rh-claim-txs) for each position you want to claim. ## 1. How Fee Sharing Works The flat 2% trade fee splits into two halves, and the token's `BagsFeeShare` is the pull-payment ledger (in WETH) for two independent streams: ```text theme={null} 2% trade fee ├── creator half (1%) -> FeeNotified(amount) -> split among claimers: │ each claimer: amount * bps[i] / 10000 │ (last claimer absorbs rounding dust) └── protocol half (1%) -> partner cut = half * partnerFeeBps / 10000 -> PartnerFeeNotified(amount) -> claimable[partner] remainder -> BagsVault (not claimable here) ``` * The **claimers** configured at launch split 100% of the creator half by their bps — the partner never reduces their share. * The **partner** (if set at launch) earns its cut from the protocol half at the launch-snapshotted `partnerFeeBps` (default `2500` = 25% of the half). See the [Partner Program](/robinhood/partner-program) guide for the partner-side workflow. Each recipient's balance accumulates in `claimable[address]` until they call `claim`. This happens automatically as trades occur in **both** phases — creators earn from bonding-curve trades and pool trades alike. Post-migration fees first accrue inside the Bags v4 hook and are periodically swept into the fee-share contract (anyone can call `hook.sweep(poolId)`). `claim` pokes that sweep for you, so you don't need a separate sweep transaction. ## 2. Read Claimable Fees Use `BagsLens.claimableOf(token, user)` for the amount claimable **right now** (already notified to the fee-share, in WETH wei): ```typescript read-claimable.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsLensAbi } from "./abi"; import type { Address } from "viem"; export async function readClaimable(token: Address, user: Address): Promise { return publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "claimableOf", args: [token, user], }); } ``` You can also read directly from the fee-share contract: | Call | Returns | | --------------------------- | ----------------------------------------------------------------------------------------------- | | `feeShare.claimable(user)` | WETH wei claimable now for `user` | | `feeShare.getClaimers()` | `(address[] claimers, uint16[] bps)` | | `feeShare.claimerBps(user)` | A user's share of the creator half in bps | | `feeShare.PARTNER()` | Partner address (`address(0)` when none) | | `curve.partnerFeeBps()` | The launch-snapshotted partner share of the protocol half (read from the token's bonding curve) | `claimableOf` counts only WETH already notified to the fee-share. Fees still sitting un-swept in the v4 hook are not included; they become claimable once `claim` (or another sweep) flushes them. The claimer list is **not immutable** — the fee-share owner can call `setClaimers` (mirrored by the `ClaimersUpdated` event). Re-read `getClaimers()` rather than caching it forever. ## 3. Claim Fees Call `claim(unwrap)` on the token's `BagsFeeShare`. Pass `unwrap: true` to receive native ETH; `false` leaves the payout as WETH. ```typescript claim-fees.ts theme={null} import "dotenv/config"; import { publicClient, getWalletClient } from "./clients"; import { bagsFeeShareAbi } from "./abi"; import { readClaimable } from "./read-claimable"; import type { Address } from "viem"; export async function claimFees(feeShare: Address, unwrap = true) { const walletClient = getWalletClient(); const txHash = await (async () => { // Simulate first so custom errors (e.g. BagsFeeShare_NothingToClaim) decode cleanly. const { request } = await publicClient.simulateContract({ account: walletClient.account, address: feeShare, abi: bagsFeeShareAbi, functionName: "claim", args: [unwrap], }); return walletClient.writeContract(request); })(); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") throw new Error("Claim reverted."); console.log(`Claimed. Tx: https://robinhoodchain.blockscout.com/tx/${txHash}`); return { txHash }; } // Example: resolve feeShare from the token, then claim as native ETH. async function main() { const token = "0xTOKEN_ADDRESS" as Address; const user = getWalletClient().account.address; const feeShare = await publicClient.readContract({ address: (await import("./addresses")).ROBINHOOD_LAUNCHPAD.factory, abi: (await import("./abi")).bagsFactoryAbi, functionName: "feeShareForToken", args: [token], }) as Address; const claimable = await readClaimable(token, user); if (claimable <= 0n) { console.log("Nothing to claim yet."); return; } console.log(`Claimable: ${claimable} WETH wei`); await claimFees(feeShare, true); } main().catch(console.error); ``` Check `claimableOf` (or `feeShare.claimable`) is greater than zero before claiming. Calling `claim` with nothing to claim reverts with `BagsFeeShare_NothingToClaim` and wastes gas. ## 4. Estimate Pending (Un-Swept) Fees For a fuller picture of what a creator will eventually receive, add the un-swept accrual sitting in the v4 hook for the token's pool. The hook exposes per-pool config and pending fees in one read: `pools(poolId)` returns `(bondingCurve, feeShare, pendingFees, minted, partner, partnerFeeBps)`. ```typescript pending-estimate.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD, ROBINHOOD_FEES } from "./addresses"; import { bagsLensAbi, bagsFeeShareAbi, bagsV4HookAbi } from "./abi"; import type { Address } from "viem"; /** Claimable-now plus an estimate of the user's cut of un-swept hook fees. */ export async function readClaimableBreakdown(token: Address, user: Address) { const state = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "getTokenState", args: [token], }); if (!state.exists) return null; const [claimable, claimers, pool] = await Promise.all([ publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "claimableOf", args: [token, user] }), publicClient.readContract({ address: state.feeShare, abi: bagsFeeShareAbi, functionName: "getClaimers" }), publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.hook, abi: bagsV4HookAbi, functionName: "pools", args: [state.poolId] }), ]); const [addresses, bps] = claimers; const i = addresses.findIndex((a) => a.toLowerCase() === user.toLowerCase()); const userBps = i === -1 ? 0 : bps[i]; // pools(poolId) -> [bondingCurve, feeShare, pendingFees, minted, partner, partnerFeeBps] const pendingFees = pool[2]; // un-swept WETH accrual for this pool (the full 2% leg) const partnerFeeBps = pool[5]; // launch-snapshotted partner share of the protocol half // On sweep the accrual splits 50/50. The creator half goes entirely to the // fee-share, split among claimers by bps. The protocol half pays the partner // partnerFeeBps of it; the remainder goes to the vault. Multiply before // dividing to limit truncation. This estimates a claimer's cut; a partner's // pending cut is (pendingFees / 2n * partnerFeeBps) / 10000n. const bpsDenom = BigInt(ROBINHOOD_FEES.bpsDenominator); const creatorHalf = pendingFees / 2n; const pendingUserEstimate = (creatorHalf * BigInt(userBps)) / bpsDenom; return { claimable, pendingFees, userBps, partnerFeeBps, isClaimer: i !== -1, feeShare: state.feeShare, pendingUserEstimate }; } ``` `pendingUserEstimate` is an estimate — the exact amount is settled when the sweep runs during a claim (`FeesSwept(poolId, bagsShare, creatorShare, partnerShare)` reports the actual split). ## Troubleshooting * **`BagsFeeShare_NothingToClaim`** — your `claimable` balance is zero. Wait for trades to accrue fees (or for a sweep to flush hook fees), and check `claimableOf` before claiming. * **`BagsFeeShare_NotAuthorized`** — only the bonding curve and hook may notify fees; you can't call `notifyFee` yourself. Just call `claim`. * **Claim succeeds but you received WETH, not ETH** — you passed `unwrap: false`. Pass `true` for native ETH, or unwrap the WETH yourself later. * **Not a claimer** — only the token's configured `claimers` and its optional `partner` accrue fees. Verify eligibility with `claimableOf(token, user)` (or `feeShare.claimable(user)`) being greater than zero. Note `feeShare.getClaimers()` lists only the configured claimers — it does **not** include the partner. For the full function, event, and error catalog, see the [Contracts Reference](/robinhood/contracts). # Contracts Reference Source: https://docs.bags.fm/robinhood/contracts Reference for the Bags smart contracts on Robinhood Chain: roles, addresses, key functions and events, custom errors, and ABI downloads. This page is the reference for the Bags contracts on Robinhood Chain. For step-by-step usage, see the [Launch](/robinhood/launch-token), [Trade](/robinhood/trade-tokens), [Read State](/robinhood/read-state), and [Claim Fees](/robinhood/claim-fees) guides. ## Addresses Protocol singletons and shared infrastructure on Robinhood Chain mainnet (chain ID `4663`): | Contract | Address | | -------------------------- | -------------------------------------------- | | `BagsFactory` (proxy) | `0xe8Cc4431adF8b5A847C113EF0c6af9043219Cb37` | | `BagsLens` | `0xC82Db941dAf90B754aecb5F7D14c683dc608d595` | | `BagsV4Hook` | `0x2380aBf72C17aABAb76480244759AC7E2932EEcC` | | `BagsVault` (proxy) | `0x4861446aa7fFd9e67a83cBbAcb1A4B70540B83Aa` | | UniversalRouter (modified) | `0x8876789976dEcBfCbBbe364623C63652db8C0904` | | V4Quoter | `0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94` | | StateView | `0xF3334192D15450CdD385c8B70e03f9A6bD9E673b` | | PoolManager | `0x8366a39CC670B4001A1121B8F6A443A643e40951` | | PositionManager | `0x58daec3116aae6D93017bAAea7749052E8a04fA7` | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | | WETH (aeWETH proxy) | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | | Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | `BagsBondingCurve` and `BagsFeeShare` are per-token beacon proxies and `BagsToken` a per-token EIP-1167 clone — resolve them via the `TokenCreated` event, `factory.curveForToken` / `factory.feeShareForToken`, or `BagsLens.getTokenState`. ## Proxy Topology Always integrate against the proxy addresses above — they are stable across upgrades. For upgrade monitoring (indexers should watch `Upgraded` events on all four): | Contract | Pattern | Upgrade surface | | ------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `BagsFactory`, `BagsVault` | UUPS + ERC1967 proxy | `Upgraded` on the proxy address | | `BagsBondingCurve` (per token) | Beacon proxy | `Upgraded` on `BagsBondingCurveBeacon` `0x8DCEcaf516C828A493C2C449c1E25F92cF80207E` (retargets **all** curves at once) | | `BagsFeeShare` (per token) | Beacon proxy | `Upgraded` on `BagsFeeShareBeacon` `0xdFf07d39C5332C602e06FA64f0A97C92fd8537e0` (retargets **all** fee-shares at once) | | `BagsToken` (per token) | EIP-1167 clone | Immutable — never upgrades | | `BagsV4Hook` | CREATE2 singleton | Not upgradeable (a replacement would only affect future launches via `factory.setHook`) | | `BagsLens` | Plain contract | Redeployed freely — pin the address above | ## ABI Downloads The full ABIs (including custom `error` definitions used for revert decoding) are published in the [`robinhood-abi-v2`](https://github.com/bagsfm/bags-idl/tree/main/robinhood-abi-v2) directory of the public [`bagsfm/bags-idl`](https://github.com/bagsfm/bags-idl) repository: | Contract | ABI | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | `BagsFactory` | [`BagsFactory.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsFactory.json) | | `BagsBondingCurve` | [`BagsBondingCurve.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsBondingCurve.json) | | `BagsFeeShare` | [`BagsFeeShare.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsFeeShare.json) | | `BagsLens` | [`BagsLens.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsLens.json) | | `BagsToken` | [`BagsToken.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsToken.json) | | `BagsV4Hook` | [`BagsV4Hook.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsV4Hook.json) | | `BagsVault` | [`BagsVault.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsVault.json) | | `BagsBeacon` | [`BagsBeacon.json`](https://github.com/bagsfm/bags-idl/blob/main/robinhood-abi-v2/BagsBeacon.json) | Import the full ABIs so your EVM library can decode custom errors (e.g. `BagsBondingCurve_SlippageExceeded`) into readable messages instead of raw revert data. ## BagsFactory Launch entry point and on-chain registry. ### Functions ```solidity theme={null} // Launch (payable) function create(string name, string symbol, string metadataURI, address partner, address[] claimers, uint16[] bps) payable returns (address token, address curve); function createAndBuy(string name, string symbol, string metadataURI, address partner, address[] claimers, uint16[] bps) payable returns (address token, address curve); // Registry (view) function curveForToken(address token) returns (address); function feeShareForToken(address token) returns (address); function tokenForPoolId(bytes32 poolId) returns (address); function getTokens(uint256 offset, uint256 limit) returns (address[]); function allTokens(uint256 index) returns (address); function allTokensLength() returns (uint256); // Config (view) — owner-settable globals, snapshotted per launch function creationFee() returns (uint256); function graduationThreshold() returns (uint256); function partnerFeeBps() returns (uint16); // partner share of the protocol half // Infrastructure wiring (view) function hook() returns (address); function vault() returns (address); function weth() returns (address); function permit2() returns (address); function poolManager() returns (address); function positionManager() returns (address); function tokenImpl() returns (address); function bondingCurveBeacon() returns (address); function feeShareBeacon() returns (address); // Owner only — affect FUTURE launches function setCreationFee(uint256 newFee); function setGraduationThreshold(uint256 newThreshold); function setPartnerFeeBps(uint16 newPartnerFeeBps); function setHook(address newHook); function setTokenImpl(address newTokenImpl); ``` ### Events ```solidity theme={null} event TokenCreated(address indexed token, address indexed curve, address indexed creator, address feeShare, address partner, bytes32 poolId, string name, string symbol, string metadataURI); event CreationFeeUpdated(uint256 newFee); event GraduationThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); event PartnerFeeBpsUpdated(uint16 oldPartnerFeeBps, uint16 newPartnerFeeBps); event HookUpdated(address indexed newHook); event TokenImplUpdated(address indexed newTokenImpl); event Upgraded(address indexed implementation); // UUPS implementation change ``` ### Key errors `BagsFactory_InsufficientCreationFee(required, sent)`, `BagsFactory_InvalidClaimers`, `BagsFactory_NoClaimers`, `BagsFactory_InvalidPartnerFeeBps(partnerFeeBps)`, `BagsFactory_InvalidGraduationThreshold(threshold)`, `BagsFactory_NoBuyValue`, `BagsFactory_BuyFailed(curve, value)`, `BagsFactory_RefundFailed(to, amount)`, `BagsFactory_FeeTransferFailed(to, amount)`. ## BagsBondingCurve Per-token pre-migration AMM (virtual `x * y = k`). Resolve its address per token. ### Functions ```solidity theme={null} // Trade function buy(uint256 minTokensOut) payable; function buyFor(address recipient, uint256 minTokensOut) payable; function sell(uint256 tokensIn, uint256 minQuoteOut); function sellFor(address recipient, uint256 tokensIn, uint256 minQuoteOut); function migrate(); // manual migration if the threshold is already met // Quotes (view) function quoteBuy(uint256 quoteIn) returns (uint256 tokensOut, uint256 feeQuote, uint256 netQuoteIn, uint256 grossUsed, uint256 refundQuote); function quoteSell(uint256 tokensIn) returns (uint256 quoteToSeller, uint256 feeQuote, uint256 grossQuoteOut); // State (view) function migrated() returns (bool); function paused() returns (bool); function currentPrice() returns (uint256); // ETH wei per whole token function bondingProgress() returns (uint256); // 0-100 function thresholdQuote() returns (uint256); // launch-snapshotted graduation target function realQuoteReserves() returns (uint256); function realTokenReserves() returns (uint256); function getVirtualReserves() returns (uint256 vToken, uint256 vQuote); function creator() returns (address); function partner() returns (address); // address(0) when none function partnerFeeBps() returns (uint16); // launch-snapshotted partner share of the protocol half function lpQuoteAmount() returns (uint256); // ETH earmarked for the migration LP function TX_FEE_BPS() returns (uint256); // 200 (2%) function LP_TOKEN_AMOUNT() returns (uint256); // 170M tokens seeded into the pool at migration ``` ### Events ```solidity theme={null} event TokensBought(address indexed buyer, address indexed recipient, uint256 grossQuoteIn, uint256 netQuoteIn, uint256 tokensOut, uint256 feeQuote, uint256 vaultFeeQuote, uint256 creatorFeeWETH, uint256 refundQuote, uint256 price, uint256 virtualTokenReserves, uint256 virtualQuoteReserves); event TokensSold(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 grossQuoteOut, uint256 netQuoteToRecipient, uint256 feeQuote, uint256 vaultFeeQuote, uint256 creatorFeeWETH, uint256 price, uint256 virtualTokenReserves, uint256 virtualQuoteReserves); event Migrated(address indexed creator, address indexed platformAdmin, address indexed token, uint256 lpQuote, uint256 lpTokens, bytes32 poolId, uint160 sqrtPriceX96); event FeesSplit(address indexed payer, address indexed vault, address indexed feeShare, uint256 vaultFeeQuote, uint256 creatorFeeWETH, uint256 partnerFeeWETH); ``` ### Key errors `BagsBondingCurve_SlippageExceeded(minExpected, actualOut)`, `BagsBondingCurve_AlreadyMigrated`, `BagsBondingCurve_NotInitialized`, `BagsBondingCurve_ThresholdNotReached(have, need)`, `BagsBondingCurve_NoQuoteSent`, `BagsBondingCurve_ZeroInputAmount`, `BagsBondingCurve_InvalidRecipient`, `EnforcedPause`. ## BagsFeeShare Per-token fee ledger (accrues in WETH): the creator half for the claimers, plus the partner's cut of the protocol half. Resolve its address per token. ### Functions ```solidity theme={null} function claim(bool unwrap); // unwrap = true pays native ETH; auto-pokes hook.sweep(poolId) function claimable(address user) returns (uint256); function getClaimers() returns (address[] addrs, uint16[] bps); function claimerBps(address user) returns (uint16); function PARTNER() returns (address); // address(0) when none function poolId() returns (bytes32); function hook() returns (address); function bondingCurve() returns (address); // Owner only function setClaimers(address[] claimers, uint16[] bps); // claimer list can change post-launch // Curve / hook only — integrators cannot call these function notifyFee(uint256 amount); function notifyPartnerFee(uint256 amount); ``` ### Events ```solidity theme={null} event Claimed(address indexed user, uint256 amount, bool unwrap); event FeeNotified(uint256 amount); // creator half delivered, split among claimers event PartnerFeeNotified(uint256 amount); // partner cut of the protocol half delivered event ClaimersUpdated(address[] claimers, uint16[] bps); event SweepFailed(address indexed hook); // claim proceeded, but the pre-claim sweep reverted ``` ### Key errors `BagsFeeShare_NothingToClaim`, `BagsFeeShare_NotAuthorized(caller)`, `BagsFeeShare_BpsSumInvalid(sum)`, `BagsFeeShare_DuplicateClaimer(claimer)`, `BagsFeeShare_TooManyClaimers(length, max)`, `BagsFeeShare_PartnerIsClaimer`, `BagsFeeShare_ClaimerHasUnpaid(claimer)`, `BagsFeeShare_NoPartner`. ## BagsLens Stateless read aggregator. Preferred entry point for reads. ### Functions ```solidity theme={null} function getTokenState(address token) returns (TokenState); function getTokenStates(address[] tokens) returns (TokenState[]); function claimableOf(address token, address user) returns (uint256); function FACTORY() returns (address); ``` ### TokenState struct ```solidity theme={null} struct TokenState { bool exists; bool migrated; address curve; address feeShare; bytes32 poolId; uint256 thresholdQuote; uint256 realQuoteReserves; uint256 realTokenReserves; uint256 virtualTokenReserves; uint256 virtualQuoteReserves; uint256 priceQuotePerToken; // freezes at migration uint256 bondingProgressPct; // 0-99 bonding; 100 migrated uint256 totalRaised; } ``` ## BagsToken Per-token ERC-20 with a fixed 1e9 supply (18 decimals). Standard ERC-20 surface (`name`, `symbol`, `decimals`, `totalSupply`, `balanceOf`, `approve`, `allowance`, `transfer`, `transferFrom`) plus: ```solidity theme={null} function metadataURI() returns (string); // reverts on non-Bags ERC-20s // EIP-2612 gasless approvals (enables one-transaction curve sells) function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s); function nonces(address owner) returns (uint256); function DOMAIN_SEPARATOR() returns (bytes32); ``` ## BagsV4Hook Singleton Uniswap v4 hook shared by all Bags pools. It takes the 2% fee on the WETH leg of post-migration swaps, locks liquidity, and routes creator fees to each token's fee-share. ### Functions ```solidity theme={null} function pools(bytes32 poolId) returns (address bondingCurve, address feeShare, uint128 pendingFees, bool minted, address partner, uint16 partnerFeeBps); function sweep(bytes32 poolId); // flush pending fees to vault + fee-share + partner (permissionless) function register(bytes32 poolId, address bondingCurve, address feeShare, address partner, uint16 partnerFeeBps); // factory-only function factory() returns (address); ``` ### Events ```solidity theme={null} event HookFeeTaken(bytes32 indexed poolId, uint256 amount); // gross WETH volume = amount * 50 event FeesSwept(bytes32 indexed poolId, uint256 bagsShare, uint256 creatorShare, uint256 partnerShare); event PoolRegistered(bytes32 indexed poolId, address indexed bondingCurve, address indexed feeShare, address partner, uint16 partnerFeeBps); event PoolMinted(bytes32 indexed poolId, address indexed currency0, address indexed currency1); ``` ### Key errors `BagsV4Hook_ExactOutputWETHSpecifiedUnsupported` (exact-out WETH is rejected by design), `BagsV4Hook_LiquidityLocked`, `BagsV4Hook_PoolNotRegistered(poolId)`, `BagsV4Hook_AlreadyRegistered(poolId)`, `BagsV4Hook_NotFactory(caller)`, `BagsV4Hook_PoolMissingWETH`. ## BagsVault Platform treasury holding native ETH (the vault portion of the protocol fee half and the launch fee). Owner-controlled withdrawals; no integrator-facing calls. ```solidity theme={null} function balance() returns (uint256 nativeBalance); // Owner only function withdraw(address to, uint256 amount); function withdrawToken(address token, address to, uint256 amount); ``` Events: `Received(from, amount)`, `Withdrawn(to, amount)`, `TokenWithdrawn(token, to, amount)`, `Upgraded(implementation)`. ## BagsBeacon Two instances (curve + fee-share) each hold the implementation address for all their beacon proxies. Integrators only need these for **upgrade monitoring** — a single `upgradeTo` retargets every live launch at once. ```solidity theme={null} function implementation() returns (address); function upgradeTo(address newImplementation); // owner only ``` Event: `Upgraded(address indexed implementation)`. ## Periphery ABIs The Uniswap-style infrastructure is not part of the Bags ABI export. These are the minimal fragments used by the [Trade Tokens](/robinhood/trade-tokens) and [Read State](/robinhood/read-state) guides. ### UniversalRouter (modified fork) Only `execute` is called directly. The v4 swap struct carries an extra `uint256 minHopPriceX36` field vs vanilla Uniswap (set it to `0`), so encode the calldata manually. ```solidity theme={null} function execute(bytes commands, bytes[] inputs, uint256 deadline) payable; ``` The v4 swap params tuple (encoded manually for `SWAP_EXACT_IN_SINGLE`): ```solidity theme={null} struct SwapExactInSingle { PoolKey poolKey; // (currency0, currency1, fee, tickSpacing, hooks) bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; uint256 minHopPriceX36; // Robinhood-only field — always 0 bytes hookData; // "0x" } ``` Command / action bytes: command `V4_SWAP = 0x10`; actions `SWAP_EXACT_IN_SINGLE = 0x06`, `SETTLE_ALL = 0x0c`, `TAKE_ALL = 0x0f`. ### Permit2 ```solidity theme={null} function approve(address token, address spender, uint160 amount, uint48 expiration); function allowance(address owner, address token, address spender) returns (uint160 amount, uint48 expiration, uint48 nonce); ``` ### V4Quoter (call via `eth_call` / simulate, not on-chain) ```solidity theme={null} function quoteExactInputSingle((PoolKey poolKey, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountOut, uint256 gasEstimate); ``` `quoteExactInputSingle` is `nonpayable`, not `view` — it must be **simulated** via `eth_call` (viem's `simulateContract`), never called as a plain read. The [Trade Tokens](/robinhood/trade-tokens) guide does this. Output already includes the 2% hook fee — apply slippage only. ### StateView ```solidity theme={null} function getSlot0(bytes32 poolId) returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); function getLiquidity(bytes32 poolId) returns (uint128 liquidity); ``` ### WETH (aeWETH proxy) WETH9-compatible interface. Never rely on its bytecode/codehash — it's an upgradeable proxy. ```solidity theme={null} function deposit() payable; function withdraw(uint256 amount); function approve(address spender, uint256 amount) returns (bool); function balanceOf(address account) returns (uint256); ``` ## Pool key parameters Bags pools use these exact values — they must match the factory or derived pool IDs will be wrong: | Parameter | Value | | ------------- | --------------------------------------- | | `fee` | `0x800000` (dynamic fee flag) | | `tickSpacing` | `60` | | `hooks` | `BagsV4Hook` address | | Pair | token / WETH, sorted by numeric address | `poolId = keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))`. Prefer the on-chain `poolId` from `TokenCreated` or `BagsLens.getTokenState`. # Launch an Index Token Source: https://docs.bags.fm/robinhood/index-tokens Launch a dividend coin on Robinhood Chain whose creator fees automatically buy a basket of tokenized assets and pay them out to holders, then register and monitor it through the Bags API. In this guide, you'll set up an **index token** (also called a *dividend coin*): a Bags token on Robinhood Chain whose creator fees are automatically converted into a basket of 1–10 tokenized assets (stocks like TSLA, NVDA, AAPL, …) and distributed pro rata to the token's holders. Holders earn dividends in the underlying assets simply by holding the token. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup). * A Bags API key from [dev.bags.fm](https://dev.bags.fm) belonging to the same Bags user that owns the token's creator wallet. * Read the [Launch a Token](/robinhood/launch-token) guide — an index token is a normal Bags V2 launch with a specific fee-claimer configuration. ## 1. How Index Tokens Work An index token runs on the standard Bags fee-share rails (see [Claim Creator Fees](/robinhood/claim-fees)), with one twist: the creator half of the trade fee is routed entirely to the Bags index bot, which converts it into dividends for holders. ```text theme={null} trades happen └── creator fees accrue (WETH) to the Bags claimer wallet └── bot claims once ≥ 0.001 ETH is claimable ├── snapshots current token holders ├── splits the ETH evenly across the basket (1-10 assets) ├── buys each asset └── transfers each purchased asset to holders, pro rata by snapshot balance ``` Key properties of a distribution cycle: * **Cadence** — the bot scans continuously (roughly once a minute) and starts a cycle once the claimable creator fees reach the minimum threshold (currently 0.001 ETH). * **Even split** — the claimed ETH is divided equally across the basket assets. * **No skim** — 100% of the claimed ETH is spent on basket assets; `distributableWei` always equals `totalWei` in the history payload. * **Pro rata payouts** — each purchased asset is transferred to holders proportionally to their snapshot balance, in batched multisend transactions. * **Excluded holders** — contracts are excluded from distributions (pools, the token itself, its fee-share contract, the bot wallet, and any address the explorer classifies as a contract). There is no minimum balance: small holders are included, but a share that rounds down to zero base units pays nothing for that asset. The basket is fixed at registration. Constituents must be tradeable tokenized assets on Robinhood Chain — the same markets available in the Bags launch flow. ## 2. Launch with the Required Claimer For the bot to ever see your token's fees, the token must be launched with the **Bags claimer wallet as its only fee claimer at 100%**: ```text theme={null} required claimer: 0x6828E679Fb51b6d0416035370aF6Ec0fb2f2055a (100%) ``` This replaces regular fee sharing — an index token routes the entire creator half of the trade fee into dividends for holders, so you cannot combine it with custom fee-share splits. Launch the token as described in [Launch a Token](/robinhood/launch-token), passing this single claimer. Initialization is rejected with `403` if the required claimer is not configured on the token. The claimer configuration is part of the launch — set it up front. ## 3. Register the Index Token After the launch transaction confirms, register the basket with [Initialize Index Token](/api-reference/init-rh-index-token). The API key's user must own the token's on-chain creator wallet. ```bash theme={null} curl --request POST \ --url https://public-api-v2.bags.fm/api/v1/evm/rh/index-token/init \ --header 'Content-Type: application/json' \ --header 'x-api-key: YOUR_API_KEY' \ --data '{ "tokenAddress": "0xYOUR_TOKEN_ADDRESS", "tokens": [ "0xTSLA_TOKEN_ADDRESS", "0xNVDA_TOKEN_ADDRESS", "0xAAPL_TOKEN_ADDRESS" ] }' ``` * `tokens` is the basket: 1–10 asset addresses, no duplicates. Any casing is accepted; addresses are normalized to EIP-55 checksum form. * A successful call returns `{ "success": true, "response": "Index token initialized successfully" }`. Error cases: | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------- | | `403` | The token's creator wallet does not belong to your user, or the required claimer is not configured | | `404` | The address is not a Bags token | | `409` | The token is already initialized as an index token | If initialization fails after a successful launch, the token still exists on-chain — just retry the `init` call once the issue is fixed. Nothing needs to be relaunched. ## 4. Verify the Registration Check that the token is registered (and read back its basket) with [Get Index Token Statuses](/api-reference/get-rh-index-token-status). The endpoint is batch-oriented — pass 1–100 addresses: ```bash theme={null} curl --request POST \ --url https://public-api-v2.bags.fm/api/v1/evm/rh/index-token/status \ --header 'Content-Type: application/json' \ --header 'x-api-key: YOUR_API_KEY' \ --data '{ "tokenAddresses": ["0xYOUR_TOKEN_ADDRESS"] }' ``` ```json theme={null} { "success": true, "response": [ { "tokenAddress": "0xYourTokenAddress", "isIndexToken": true, "tokens": ["0xTSLA...", "0xNVDA...", "0xAAPL..."] } ] } ``` `tokens` is empty when `isIndexToken` is `false`. ## 5. Monitor Distribution Cycles Once trading generates enough fees, completed cycles appear in [Get Index Token History](/api-reference/get-rh-index-token-history), newest first: ```bash theme={null} curl --request GET \ --url 'https://public-api-v2.bags.fm/api/v1/evm/rh/index-token/history?tokenAddress=0xYOUR_TOKEN_ADDRESS&limit=20' \ --header 'x-api-key: YOUR_API_KEY' ``` Each item is one full cycle: | Field | Contents | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `claim` | The fee claim funding the cycle — `claimedWei` (ETH in wei), `txHash`, mined/finalized timestamps | | `buys[]` | One entry per basket asset — `ethInWei` spent, `boughtAmountRaw` received (base units), `txHash` | | `distribution` | `totalWei` / `distributableWei` (always equal), `payoutTxHashes`, `completedAt`, and the holder `snapshot` (`recipientCount`, `excludedHolderCount`, `includedSupply`, `blockNumber`) | | `topRecipients[]` | Top 10 recipients by snapshot balance, with per-asset amounts received | For pagination, pass `nextCursor` back as `cursor`; it is `null` when the history is exhausted. Only completed cycles with confirmed claims are returned — a cycle that is still claiming, buying, or transferring does not appear yet. `snapshot.includedSupply` is the pro-rata denominator: the token supply held by eligible (non-excluded) holders at the snapshot. A holder's payout per asset is `boughtAmountRaw * holderBalance / includedSupply`, truncated. ## Troubleshooting * **`403` on init** — either the launch was made from a wallet that doesn't belong to your Bags user, or the token wasn't launched with the required claimer at 100%. The claimer set is part of launch configuration; verify with `feeShare.getClaimers()`. * **`409` on init** — the token is already registered. The basket is immutable; there is no re-init. * **`isIndexToken` is `false` after a successful init** — statuses are cached briefly server-side; re-check after a few seconds. * **No history items** — cycles only complete after claimable fees reach the 0.001 ETH threshold and the buy + distribution finish. Check accrued fees via [Get Claimable Positions](/api-reference/get-rh-claimable-positions) for the claimer wallet, and note the empty history payload (`items: []`, `hasMore: false`) is expected for a fresh token. * **A holder received nothing** — contract addresses are excluded, and very small balances can truncate to zero base units for a given asset. Both are expected behavior. # Launch a Token Source: https://docs.bags.fm/robinhood/launch-token Launch a Bags token on Robinhood Chain by calling the BagsFactory directly with viem, with an optional atomic initial buy and configurable fee claimers. In this guide, you'll launch a token on Robinhood Chain by calling `BagsFactory` directly. A single transaction deploys the token, its bonding curve, and its fee-share contract, mints the fixed supply to the curve, and registers everything on-chain. You can optionally include an **atomic initial buy** in the same transaction. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup) (`chain.ts`, `addresses.ts`, `clients.ts`, and the ABIs). * Read the [Overview](/robinhood/overview) for the token lifecycle and fee model. * ETH in your wallet to cover the creation fee (default 0.02 ETH — read it live), any initial buy, and gas. ## 1. How Launching Works `BagsFactory` exposes two launch entry points: | Function | Use when | `value` to send | | -------------- | ---------------------------- | -------------------------------- | | `create` | No initial buy | `creationFee` (surplus refunded) | | `createAndBuy` | You want an atomic first buy | `creationFee + initialBuyWei` | Both take the same arguments: ```solidity theme={null} function create( string name, string symbol, string metadataURI, address partner, // address(0) for none address[] claimers, // fee-share recipients uint16[] bps // parallel to claimers; must sum to 10000 ) external payable returns (address token, address curve); ``` The partner's fee rate is **not** a launch parameter: the factory's global `partnerFeeBps` (default `2500`) is snapshotted into the launch, and the partner is paid from the **protocol half** of trading fees — it never reduces the claimers' share. See section 6. With `createAndBuy`, the factory spends the entire surplus above `creationFee` on an atomic `buyFor(msg.sender, 1)` — there is no front-run window, and if the buy would cross the graduation threshold the excess is auto-refunded. The returned `token` and `curve` addresses, plus the `feeShare`, `partner`, and `poolId`, must be read from the **`TokenCreated` event** in the receipt. Per-launch addresses are not predictable before the launch. ## 2. Fee Claimers Rules The `claimers` / `bps` arrays configure who earns the creator half (1%) of trading fees. The factory enforces: 1. **1 to 100 claimers**, with `claimers.length == bps.length`. 2. Each claimer address is **valid, non-zero, and unique**, and not equal to the `partner`. 3. Each `bps` is a `uint16`, and **all `bps` sum to exactly `10000`** (100%). The factory enforces only the sum — individual shares of `0` are accepted on-chain. The client validator below additionally requires each share to be `>= 1` as a sensible default (a `0`-bps claimer would never earn). The simplest setup is a single claimer (the creator) at `10000` bps. Validate client-side before signing — an on-chain revert still costs gas. When splitting a percentage across claimers, have the **last** claimer absorb any rounding remainder so the total lands on exactly `10000`. ## 3. Prepare Metadata `metadataURI` should point to a JSON document describing the token (name, symbol, description, image, socials), typically hosted on IPFS. Upload it however you like and pass the resulting URI (e.g. `https://ipfs.io/ipfs/...`). This guide assumes you already have a `metadataURI`. ## 4. The Launch Script Save this as `launch-token.ts`. It validates claimers, reads the live creation fee, simulates, sends, and parses the receipt. ```typescript launch-token.ts theme={null} import "dotenv/config"; import { getAddress, isAddress, parseEther, parseEventLogs, zeroAddress, type Address, } from "viem"; import { publicClient, getWalletClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsFactoryAbi, bagsBondingCurveAbi } from "./abi"; const TOTAL_BPS = 10_000; const MAX_CLAIMERS = 100; interface ClaimerAllocation { address: Address; bps: number; } /** Client-side mirror of the factory's claimer rules. Returns an error string, or null when valid. */ function validateClaimers(claimers: ClaimerAllocation[], partner?: Address): string | null { if (claimers.length === 0) return "Add at least one fee claimer."; if (claimers.length > MAX_CLAIMERS) return `At most ${MAX_CLAIMERS} claimers.`; // Only reject claimers matching a *real* partner — zeroAddress means "no partner". const partnerKey = partner && partner.toLowerCase() !== zeroAddress ? partner.toLowerCase() : null; const seen = new Set(); for (const c of claimers) { if (!isAddress(c.address)) return `"${c.address}" is not a valid address.`; const key = c.address.toLowerCase(); if (key === zeroAddress) return "The zero address cannot be a claimer."; if (partnerKey && key === partnerKey) return "The partner cannot also be a claimer."; if (seen.has(key)) return "Claimer addresses must be unique."; seen.add(key); if (!Number.isInteger(c.bps) || c.bps < 1 || c.bps > TOTAL_BPS) return "Each claimer needs a whole bps share between 1 and 10000."; } const total = claimers.reduce((sum, c) => sum + c.bps, 0); if (total !== TOTAL_BPS) return `bps must sum to exactly 10000 (currently ${total}).`; return null; } async function launchToken(params: { name: string; symbol: string; metadataURI: string; claimers: ClaimerAllocation[]; /** Optional partner, paid from the protocol half at the factory's snapshotted partnerFeeBps. */ partner?: Address; /** ETH spent on an atomic initial buy. Use 0n for no buy. */ initialBuyEth?: bigint; }) { const { name, symbol, metadataURI, claimers } = params; const initialBuyWei = params.initialBuyEth ?? 0n; const partner = params.partner ?? zeroAddress; const claimersError = validateClaimers(claimers, partner); if (claimersError) throw new Error(claimersError); const walletClient = getWalletClient(); const account = walletClient.account; // The creation fee is owner-settable — read it live right before signing. const creationFee = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName: "creationFee", }); // partner defaults to address(0) (no partner) unless one is passed in. const args = [ name, symbol, metadataURI, partner, claimers.map((c) => getAddress(c.address)), claimers.map((c) => c.bps), ] as const; const value = creationFee + initialBuyWei; const functionName = initialBuyWei > 0n ? "createAndBuy" : "create"; console.log(`Launching $${symbol} via ${functionName} (fee ${creationFee} wei)...`); // Simulate first so reverts surface as decoded custom errors before signing. const { request } = await publicClient.simulateContract({ account, address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName, args, value, }); const txHash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") { throw new Error("Launch transaction reverted on-chain. No token was created."); } // Clone addresses only exist in the TokenCreated event. const [created] = parseEventLogs({ abi: bagsFactoryAbi, logs: receipt.logs, eventName: "TokenCreated", }); if (!created) throw new Error("Launch confirmed but no TokenCreated event found."); // A large enough initial buy can graduate the token in the same tx. const migratedLogs = parseEventLogs({ abi: bagsBondingCurveAbi, logs: receipt.logs, eventName: "Migrated", }); const result = { txHash, token: created.args.token as Address, curve: created.args.curve as Address, feeShare: created.args.feeShare as Address, partner: created.args.partner as Address, poolId: created.args.poolId as `0x${string}`, migrated: migratedLogs.length > 0, }; console.log("Token launched!"); console.log(" token :", result.token); console.log(" curve :", result.curve); console.log(" feeShare:", result.feeShare); console.log(" partner :", result.partner); console.log(" poolId :", result.poolId); console.log(" migrated:", result.migrated); console.log(` explorer: https://robinhoodchain.blockscout.com/tx/${txHash}`); return result; } // Example: launch with all fees to the creator and a small initial buy. launchToken({ name: "My Token", symbol: "MTK", metadataURI: "https://ipfs.io/ipfs/YOUR_METADATA_CID", claimers: [ { address: "0xYOUR_WALLET_ADDRESS", bps: 10000 }, // creator gets 100% ], initialBuyEth: parseEther("0.01"), }).catch(console.error); ``` ## 5. Run Your Script ```bash theme={null} npx ts-node launch-token.ts ``` On success you'll see the token, curve, feeShare, and poolId addresses. Save the `token` address — it's the identifier you'll use everywhere else. ## 6. Splitting Fees Among Multiple Claimers To share the creator side with additional wallets, list each with its bps. The example below gives the creator 50%, and two collaborators 30% and 20%: ```typescript theme={null} launchToken({ name: "Shared Token", symbol: "SHARE", metadataURI: "https://ipfs.io/ipfs/YOUR_METADATA_CID", claimers: [ { address: "0xCREATOR", bps: 5000 }, { address: "0xCOLLAB_ONE", bps: 3000 }, { address: "0xCOLLAB_TWO", bps: 2000 }, // absorbs rounding; total = 10000 ], }); ``` To include a **partner**, pass a non-zero `partner` address. The partner's cut comes from the **protocol half** of trading fees, not from the claimers: the factory's global `partnerFeeBps` (default `2500` = 25% of the protocol half = 0.25% of volume) is snapshotted at launch, the partner accrues that share in the token's `BagsFeeShare`, and the vault receives the remainder. The claimers always split the full creator half regardless of partner. The partner must not also appear in `claimers`. See the [Partner Program](/robinhood/partner-program) guide for the full partner workflow (tracking tokens, reading earnings, claiming). ```typescript theme={null} launchToken({ name: "Partnered Token", symbol: "PART", metadataURI: "https://ipfs.io/ipfs/YOUR_METADATA_CID", partner: "0xPARTNER_ADDRESS", // earns the snapshotted partnerFeeBps of the protocol half claimers: [ { address: "0xCREATOR", bps: 10000 }, // creator still gets the full creator half ], }); ``` ## 7. What Happens Next After launch, the token trades on its bonding curve. Buyers send ETH to the curve to receive tokens; the creator side of every trade fee accrues to the `feeShare` contract. When enough ETH is raised, the token graduates to a Uniswap v4 pool. * To trade the token, see [Trade Tokens](/robinhood/trade-tokens). * To read live state (price, bonding progress, migration status), see [Read State & Discover Tokens](/robinhood/read-state). * To claim accrued fees, see [Claim Creator Fees](/robinhood/claim-fees). ## Troubleshooting * **`BagsFactory_InsufficientCreationFee`** — you sent less than `creationFee()`. Read the fee live and set `value` accordingly. * **`BagsFactory_NoClaimers`** — the `claimers` array is empty. Pass at least one claimer. * **`BagsFactory_InvalidClaimers`** — the config failed a rule in section 2: `claimers.length != bps.length`, a duplicate or zero address, a claimer equal to the `partner`, more than 100 claimers, or `bps` that don't sum to `10000`. * **`BagsFactory_BuyFailed(curve, value)`** — the atomic initial buy in `createAndBuy` reverted (e.g. slippage or curve state). Lower the buy value, or call `create` and buy separately. * **`BagsFactory_NoBuyValue`** — you called `createAndBuy` with `value == creationFee` (nothing left to buy with). Either add buy value or call `create`. * **Simulation reverts before sending** — the decoded custom error tells you exactly which rule failed; fix the inputs and retry. Simulating first means you never waste gas on a doomed launch. For the full function, event, and error catalog, see the [Contracts Reference](/robinhood/contracts). # Robinhood Chain Overview Source: https://docs.bags.fm/robinhood/overview How the Bags protocol works on Robinhood Chain: token lifecycle, fees, chain facts, and the full contract address book for third-party integrators. Bags is deployed on **Robinhood Chain** as a set of public, permissionless smart contracts. Unlike the Solana integration (which is driven by the Bags REST API and SDK), the Robinhood Chain integration is **fully on-chain**: you launch, trade, and claim fees by calling the contracts directly with a standard EVM library such as [viem](https://viem.sh) or [ethers](https://docs.ethers.org). These guides show third-party developers how to integrate launching and trading against those contracts. No API key is required to interact with the chain. "Robinhood" here means **Robinhood Chain**, an EVM Layer 2 (Arbitrum Orbit) with chain ID `4663` and ETH as the native gas token. It is a separate chain from Solana and from BNB Chain. ## What you can do * **Launch a token** through the `BagsFactory` (with an optional atomic initial buy). See [Launch a Token](/robinhood/launch-token). * **Trade** a token against its bonding curve before graduation, and against a Uniswap v4 pool after graduation. See [Trade Tokens](/robinhood/trade-tokens). * **Read state and discover tokens** through `BagsLens` and the factory registry. See [Read State & Discover Tokens](/robinhood/read-state). * **Claim creator fees** from a token's `BagsFeeShare`. See [Claim Creator Fees](/robinhood/claim-fees). Start with the [Environment Setup](/robinhood/setup) guide to configure your clients and ABIs. ## Token lifecycle Every Bags token moves through two trading phases. It is created on a **bonding curve**; once enough ETH has been raised it **graduates** (migrates) into a Uniswap v4 pool with permanently locked liquidity. ```mermaid theme={null} flowchart LR create["factory.create / createAndBuy"] --> curve["Bonding curve phase (buy / sell vs BagsBondingCurve)"] curve -->|"realQuoteReserves >= thresholdQuote"| migrate["Graduation (Migrated event)"] migrate --> pool["Uniswap v4 pool (swap via modified UniversalRouter)"] curve -. "1% creator fee" .-> feeshare["BagsFeeShare.claim"] pool -. "1% creator fee" .-> feeshare ``` 1. **Creation** — `BagsFactory.create` (or `createAndBuy`) deploys a per-token `BagsToken`, `BagsBondingCurve`, and `BagsFeeShare`, mints the fixed supply to the curve, and registers the token. 2. **Bonding curve** — trades run against the token's `BagsBondingCurve` using a virtual `x * y = k` AMM. Buys send native ETH; sells return native ETH. 830M of the 1B supply is sold on the curve. 3. **Graduation** — when the curve's real ETH reserves reach `thresholdQuote`, the next buy triggers migration: the remaining 170M tokens plus the full raise are deposited into a Uniswap v4 pool (with the Bags hook) at the curve's final price, the LP is locked, and the curve is paused. The curve emits a `Migrated` event. 4. **Uniswap v4** — after graduation, trades route through the Robinhood-modified UniversalRouter against the token/WETH pool. Determine the current phase by reading the token's `migrated` flag from `BagsLens.getTokenState`, and route trades accordingly. Never assume a phase. ## Fee model A flat **2% fee is charged on the ETH/WETH leg of every trade, in both phases**, split into two halves: | Portion | Rate | Destination | | ------------- | ------------------- | ------------------------------------------------------------------------------------------------- | | Creator half | 1% (50% of the fee) | The token's `BagsFeeShare` (accrues in WETH) — split among the fee claimers by their basis points | | Protocol half | 1% (50% of the fee) | Split between the optional **partner** and `BagsVault` | * On **buys**, the fee is taken from the ETH input before it enters the curve/pool. * On **sells**, the fee is taken from the ETH output. * The **creator half** goes entirely to the token's fee claimers, pro-rata by the basis points set at launch. Claim it with [`BagsFeeShare.claim`](/robinhood/claim-fees). * The **protocol half** is split: if the launch has a partner, the partner receives `protocolHalf x partnerFeeBps / 10000` (accrued in WETH in `BagsFeeShare`); the remainder goes to `BagsVault`. `partnerFeeBps` is a factory global (default `2500` = 0.25% of volume) snapshotted per launch. There is also a one-time **launch fee** (`creationFee`, default 0.02 ETH) paid to the vault when a token is created. `factory.creationFee()`, `factory.graduationThreshold()`, and `factory.partnerFeeBps()` are owner-settable globals on the factory and can change; each launch snapshots them at `create()` time. Each curve exposes its own `thresholdQuote()` and `partnerFeeBps()` — the per-launch snapshots. Always read these live from the contracts before rendering or signing — never hardcode. ## Chain facts | Fact | Value | | --------------------- | ----------------------------------------------------------------------------------- | | Network | Robinhood Chain (Arbitrum Orbit L2) | | Chain ID | `4663` (`0x1237`) | | Native currency | ETH (18 decimals) | | RPC (public) | `https://rpc.mainnet.chain.robinhood.com` (rate-limited) | | Explorer | [robinhoodchain.blockscout.com](https://robinhoodchain.blockscout.com) (Blockscout) | | Block time | \~100 ms, first-come-first-served sequencer | | Protocol deploy block | `7887312` (lower bound for any log scan) | Because the sequencer is first-come-first-served and blocks are \~100 ms: * Use **timestamps**, not block numbers, for transaction deadlines. * The public RPC is rate-limited — batch reads with viem multicall (see [Setup](/robinhood/setup)). * Priority fees buy nothing; gas is negligible. ## Contract address book These are the protocol singletons and shared infrastructure on Robinhood Chain mainnet. | Contract | Address | Role | | --------------- | -------------------------------------------- | ---------------------------------------------------------- | | `BagsFactory` | `0xe8Cc4431adF8b5A847C113EF0c6af9043219Cb37` | Launch entry point + on-chain registry | | `BagsLens` | `0xC82Db941dAf90B754aecb5F7D14c683dc608d595` | Batched read aggregator (state, claimable) | | `BagsV4Hook` | `0x2380aBf72C17aABAb76480244759AC7E2932EEcC` | Singleton Uniswap v4 hook; takes the 2% post-migration fee | | `BagsVault` | `0x4861446aa7fFd9e67a83cBbAcb1A4B70540B83Aa` | Platform treasury (native ETH) | | UniversalRouter | `0x8876789976dEcBfCbBbe364623C63652db8C0904` | **Robinhood-modified fork** — post-migration swaps | | V4Quoter | `0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94` | Off-chain quotes for pool swaps | | StateView | `0xF3334192D15450CdD385c8B70e03f9A6bD9E673b` | Pool state reads (`getSlot0`, `getLiquidity`) | | PoolManager | `0x8366a39CC670B4001A1121B8F6A443A643e40951` | Uniswap v4 singleton (`Swap` logs) | | PositionManager | `0x58daec3116aae6D93017bAAea7749052E8a04fA7` | Uniswap v4 periphery (migration LP mint) | | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | Canonical Permit2 (router spending route) | | WETH | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | aeWETH proxy (WETH9-compatible interface) | | Multicall3 | `0xcA11bde05977b3631167028862bE2a173976CA11` | Canonical multicall | `BagsFactory` and `BagsVault` are UUPS proxies — the addresses above are stable across upgrades, so always integrate against them (never against implementation addresses). The UniversalRouter above is a **modified fork**. Its v4 swap struct carries an extra `minHopPriceX36` field, so stock Uniswap SDK calldata will revert. Two other router look-alikes exist on this chain — only this address is correct. See [Trade Tokens](/robinhood/trade-tokens) for the exact encoding. ### Per-token contracts `BagsBondingCurve` and `BagsFeeShare` are deployed **per launch** as beacon proxies, and `BagsToken` as an immutable [EIP-1167 minimal proxy clone](https://eips.ethereum.org/EIPS/eip-1167). Their addresses are **not predictable** before launch. Always resolve them from: * The `TokenCreated` event in the launch receipt (returns `token`, `curve`, `feeShare`, `partner`, `poolId`), or * The factory registry: `factory.curveForToken(token)` and `factory.feeShareForToken(token)`, or * `BagsLens.getTokenState(token)` (returns `curve`, `feeShare`, `poolId`, and more). Never hardcode per-token addresses. ## Token supply Every Bags token has a fixed supply of **1,000,000,000 (1e9) tokens**, each with 18 decimals (`1_000_000_000 * 1e18` base units). Fully diluted valuation (FDV) equals the spot price in ETH per token multiplied by `1e9`. ## Next steps Configure your viem clients and ABIs for Robinhood Chain. Create a token through the BagsFactory. Buy and sell across the bonding curve and Uniswap v4. Functions, events, errors, and ABIs. # Partner Program Source: https://docs.bags.fm/robinhood/partner-program Earn a share of trading fees as a Robinhood Chain launch partner: how partner economics work, how to launch with a partner, track your tokens, and claim earnings. If your app or platform launches Bags tokens on behalf of creators, you can register yourself as the **partner** on every launch and earn a share of the trading fees for the lifetime of the token — without taking anything away from the creator. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup). * Read the [Overview](/robinhood/overview) for the fee model and lifecycle. ## 1. How Partner Economics Work Every trade pays a flat 2% fee on the ETH/WETH leg, split into two halves. The partner is paid from the **protocol half** — the creator half always goes 100% to the token's fee claimers: ```text theme={null} 2% trade fee ├── creator half (1%) -> the token's fee claimers (untouched by the partner) └── protocol half (1%) ├── partner: half * partnerFeeBps / 10000 -> accrues in the token's BagsFeeShare (WETH) └── remainder -> BagsVault ``` * `partnerFeeBps` is a **factory global** (`factory.partnerFeeBps()`, default `2500` = 25% of the protocol half = **0.25% of trade volume**). * It is **snapshotted at launch**: each token keeps the value that was set when it was created, readable per token via `curve.partnerFeeBps()`. Later factory changes only affect future launches. * Partner earnings accrue in **both phases** — bonding-curve trades and post-graduation Uniswap v4 swaps alike. A launch has at most **one** partner, set once at `create` time and immutable afterwards. The partner address must not also appear in the launch's `claimers` array. ## 2. Launch With Your Partner Address Pass your address as the `partner` argument to `create` / `createAndBuy` — that's the entire integration on the launch side: ```typescript theme={null} launchToken({ name: "Creator Token", symbol: "CTK", metadataURI: "https://ipfs.io/ipfs/METADATA_CID", partner: "0xYOUR_PLATFORM_WALLET", // you earn partnerFeeBps of the protocol half claimers: [ { address: "0xCREATOR_WALLET", bps: 10000 }, // creator keeps the full creator half ], }); ``` See [Launch a Token](/robinhood/launch-token) for the full `launchToken` script. Use a dedicated, secured wallet for the partner address — it cannot be changed after launch. ## 3. Track the Tokens You Partner On The `TokenCreated` event carries the `partner` field, so index your launches from factory logs and filter on it (`partner` is not an indexed topic, so filter client-side): ```typescript partner-tokens.ts theme={null} import { parseAbiItem, type Address } from "viem"; import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD, ROBINHOOD_DEPLOY_BLOCK } from "./addresses"; const tokenCreatedEvent = parseAbiItem( "event TokenCreated(address indexed token, address indexed curve, address indexed creator, address feeShare, address partner, bytes32 poolId, string name, string symbol, string metadataURI)" ); /** All launches where `partner` is your address. */ export async function getPartnerTokens(partner: Address, fromBlock: bigint = ROBINHOOD_DEPLOY_BLOCK) { const logs = await publicClient.getLogs({ address: ROBINHOOD_LAUNCHPAD.factory, event: tokenCreatedEvent, fromBlock, toBlock: "latest", }); return logs .filter((log) => log.args.partner?.toLowerCase() === partner.toLowerCase()) .map((log) => ({ token: log.args.token!, curve: log.args.curve!, feeShare: log.args.feeShare!, poolId: log.args.poolId!, symbol: log.args.symbol!, })); } ``` For an ongoing feed, checkpoint the last scanned block as described in [Read State & Discover Tokens](/robinhood/read-state). To verify a single token's partner setup, read `curve.partner()` and `curve.partnerFeeBps()` from its bonding curve. ## 4. Read Partner Earnings Partner earnings accrue in each token's `BagsFeeShare` and are read exactly like claimer balances: ```typescript partner-earnings.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsLensAbi } from "./abi"; import type { Address } from "viem"; /** Claimable WETH (wei) per token, batched through multicall. */ export async function readPartnerEarnings(partner: Address, tokens: Address[]) { const amounts = await Promise.all( tokens.map((token) => publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "claimableOf", args: [token, partner], }) ) ); return tokens.map((token, i) => ({ token, claimable: amounts[i] })); } ``` Two useful mirrors when accounting: * Pre-graduation, each curve trade emits `FeesSplit(payer, vault, feeShare, vaultFeeQuote, creatorFeeWETH, partnerFeeWETH)` — your cut is `partnerFeeWETH`. * The fee-share emits `PartnerFeeNotified(amount)` whenever your cut is delivered. Like claimer balances, `claimableOf` counts only fees already notified to the fee-share. Post-graduation fees still un-swept in the v4 hook become claimable once a sweep runs — your `claim` triggers one automatically. Your pending share of the un-swept amount is `(pendingFees / 2) * partnerFeeBps / 10000`, with `pendingFees` and `partnerFeeBps` from `hook.pools(poolId)`. ## 5. Claim Partner Fees Claiming is identical to claimer flow — call `claim(unwrap)` on each token's `BagsFeeShare` from the partner wallet (see [Claim Creator Fees](/robinhood/claim-fees) for the full script): ```typescript theme={null} import { bagsFeeShareAbi } from "./abi"; const { request } = await publicClient.simulateContract({ account: partnerWallet.account, address: feeShare, abi: bagsFeeShareAbi, functionName: "claim", args: [true], // true = receive native ETH, false = keep WETH }); await partnerWallet.writeContract(request); ``` Fee shares are per token, so claim each token separately. Skip tokens where `claimableOf` is zero — claiming with nothing to claim reverts with `BagsFeeShare_NothingToClaim`. ## Troubleshooting * **`BagsFeeShare_NothingToClaim`** — no partner fees accrued yet on this token, or you already claimed. Check `claimableOf(token, partner)` first. * **Your address earns nothing on a token** — confirm you were actually set as the partner at launch: `curve.partner()` must return your address. The partner cannot be added retroactively. * **Launch reverts with `BagsFactory_InvalidClaimers`** — the partner address also appears in `claimers`. Remove it; the partner is paid from the protocol half, not the claimer split. * **Earnings look \~0.25% of volume, not 1%** — that's correct: the default `partnerFeeBps` of `2500` pays 25% of the 1% protocol half. The creator half is never shared with partners. For the full function, event, and error catalog, see the [Contracts Reference](/robinhood/contracts). # Read State & Discover Tokens Source: https://docs.bags.fm/robinhood/read-state Query Bags token state with BagsLens, discover tokens through the factory registry and TokenCreated events, derive pool IDs, and read post-migration prices. In this guide, you'll read on-chain state for Bags tokens on Robinhood Chain: a single-call state snapshot via `BagsLens`, token discovery through the factory registry and events, pool ID derivation, and post-migration price reads. All of these are read-only — no wallet or gas required. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup) (`publicClient`, addresses, ABIs). * Read the [Overview](/robinhood/overview) for the token lifecycle. The `publicClient` from Setup batches parallel `readContract` calls into single multicall3 requests. Prefer many small reads with `Promise.all` over hand-rolled multicall — batching handles it and keeps you under the public-RPC rate limit. ## 1. Read a Token's State with BagsLens `BagsLens.getTokenState` returns everything you need about a token in one `eth_call`. It's the recommended entry point for any token view. ```typescript read-state.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsLensAbi } from "./abi"; import type { Address } from "viem"; export async function readTokenState(token: Address) { const state = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "getTokenState", args: [token], }); if (!state.exists) return null; // not a Bags token return state; } ``` The returned `TokenState` struct: | Field | Type | Meaning | | ---------------------- | --------- | ----------------------------------------------------------------------------------------------- | | `exists` | `bool` | `false` ⇒ not a Bags token | | `migrated` | `bool` | `true` ⇒ graduated to a Uniswap v4 pool | | `curve` | `address` | The token's `BagsBondingCurve` | | `feeShare` | `address` | The token's `BagsFeeShare` | | `poolId` | `bytes32` | Uniswap v4 pool ID | | `thresholdQuote` | `uint256` | ETH (wei) needed to graduate | | `realQuoteReserves` | `uint256` | Real ETH reserves the curve holds right now (wei) | | `realTokenReserves` | `uint256` | Tokens left on the curve | | `virtualTokenReserves` | `uint256` | Virtual token reserve (curve math) | | `virtualQuoteReserves` | `uint256` | Virtual ETH reserve (curve math) | | `priceQuotePerToken` | `uint256` | Spot price, ETH wei per whole token | | `bondingProgressPct` | `uint256` | Graduation progress: `0`–`99` while bonding, reaching `100` only once `migrated` is `true` | | `totalRaised` | `uint256` | Cumulative ETH raised into the curve over its lifetime, measured against `thresholdQuote` (wei) | `priceQuotePerToken` **freezes at migration** — it is only valid while `migrated` is `false`. For migrated tokens, read the live price from the pool (see section 5). ### Batch multiple tokens For lists, use `getTokenStates` to fetch many tokens in one call: ```typescript theme={null} const states = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "getTokenStates", args: [tokens], // Address[] }); ``` ## 2. Discover Tokens via the Factory Registry `BagsFactory` maintains an append-only registry of every launch. Because it's append-only, the **tail** of the list is the newest launches. ```typescript list-tokens.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsFactoryAbi, bagsLensAbi } from "./abi"; /** The newest `limit` launches with live state, in one batched round-trip. */ export async function listNewestTokens(limit: number) { const total = Number( await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName: "allTokensLength", }) ); if (total === 0) return { items: [], total: 0 }; const count = Math.min(limit, total); const offset = BigInt(total - count); // tail = newest const tokens = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName: "getTokens", args: [offset, BigInt(count)], }); const states = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "getTokenStates", args: [tokens], }); // Newest first. const items = tokens .map((token, i) => ({ token, state: states[i] })) .filter((row) => row.state.exists) .reverse(); return { items, total }; } ``` Other registry lookups: | Call | Returns | | ---------------------------------- | ---------------------------------------------------------------------------- | | `factory.allTokensLength()` | Total number of launches | | `factory.getTokens(offset, limit)` | A page of token addresses | | `factory.allTokens(index)` | Token address at an index | | `factory.curveForToken(token)` | The token's bonding curve | | `factory.feeShareForToken(token)` | The token's fee-share contract | | `factory.tokenForPoolId(poolId)` | The token for a given pool ID | | `factory.creationFee()` | Current launch fee (wei) | | `factory.graduationThreshold()` | Default graduation threshold (wei) for future launches | | `factory.partnerFeeBps()` | Current partner share of the protocol fee half (bps), snapshotted per launch | ## 3. Read Immutable Token Info Name, symbol, and metadata URI never change, so read them once and cache. Non-Bags ERC-20s revert on `metadataURI`, so treat that as absent. ```typescript token-info.ts theme={null} import { publicClient } from "./clients"; import { bagsTokenAbi } from "./abi"; import type { Address } from "viem"; export async function readTokenInfo(token: Address) { const [name, symbol, metadataURI, totalSupply] = await Promise.all([ publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "name" }), publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "symbol" }), publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "metadataURI" }).catch(() => ""), publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "totalSupply" }), ]); return { name, symbol, metadataURI, totalSupply }; } ``` ## 4. Discover via TokenCreated Events You can also index launches directly from `TokenCreated` logs (useful for backfilling history or building a stream). Start no earlier than the protocol deploy block. ```typescript index-launches.ts theme={null} import { parseAbiItem } from "viem"; import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD, ROBINHOOD_DEPLOY_BLOCK } from "./addresses"; const tokenCreatedEvent = parseAbiItem( "event TokenCreated(address indexed token, address indexed curve, address indexed creator, address feeShare, address partner, bytes32 poolId, string name, string symbol, string metadataURI)" ); export async function getLaunches(fromBlock: bigint = ROBINHOOD_DEPLOY_BLOCK) { const logs = await publicClient.getLogs({ address: ROBINHOOD_LAUNCHPAD.factory, event: tokenCreatedEvent, fromBlock, toBlock: "latest", }); return logs.map((log) => ({ token: log.args.token, curve: log.args.curve, creator: log.args.creator, feeShare: log.args.feeShare, partner: log.args.partner, poolId: log.args.poolId, name: log.args.name, symbol: log.args.symbol, metadataURI: log.args.metadataURI, blockNumber: log.blockNumber, txHash: log.transactionHash, })); } ``` For a live feed, checkpoint the last scanned block and only query `[lastScanned + 1, latest]` on each poll rather than re-scanning from the deploy block every time. ### Indexer notes * **Trade events are self-contained.** `TokensBought` and `TokensSold` embed the post-trade price, virtual reserves, and full fee breakdown (vault / creator / partner), so you can index curve trades without extra state reads per event. * **Phase flip.** `Migrated` on the curve is the switch point: stop consuming curve events and start consuming `PoolManager` `Swap` logs (filtered by the token's `poolId`) and the hook's `HookFeeTaken`. * **Pool-phase volume.** The hook takes 2% of the WETH leg on every pool swap, so gross WETH volume per swap = `HookFeeTaken.amount x 50`. * **Fee claimers are not in events at launch.** Call `feeShare.getClaimers()` after `TokenCreated`, and watch `ClaimersUpdated` — the list can change. * **Upgrade monitoring.** `BagsFactory` and `BagsVault` are UUPS proxies, and per-token curves/fee-shares point at two shared beacons. Watch `Upgraded` on the factory/vault proxies and on both beacons (`BagsBondingCurveBeacon`, `BagsFeeShareBeacon` — addresses in the [Contracts Reference](/robinhood/contracts)) to detect implementation changes. ## 5. Post-Migration Price from the Pool After migration, read the live spot price from the pool's `slot0` via `StateView`. Never fall back to the frozen lens price for migrated tokens. Add the `StateView` fragment to `abi/periphery.ts`: ```typescript theme={null} export const stateViewAbi = [ { type: "function", name: "getSlot0", inputs: [{ name: "poolId", type: "bytes32" }], outputs: [ { name: "sqrtPriceX96", type: "uint160" }, { name: "tick", type: "int24" }, { name: "protocolFee", type: "uint24" }, { name: "lpFee", type: "uint24" }, ], stateMutability: "view", }, { type: "function", name: "getLiquidity", inputs: [{ name: "poolId", type: "bytes32" }], outputs: [{ name: "liquidity", type: "uint128" }], stateMutability: "view", }, ] as const; ``` Then read `slot0` and convert `sqrtPriceX96` to ETH per whole token: ```typescript pool-price.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { stateViewAbi } from "./abi/periphery"; import { isTokenCurrency0 } from "./poolKey"; import type { Address, Hex } from "viem"; const Q96 = 2 ** 96; /** ETH per whole token from a v4 sqrtPriceX96 price. Returns null on failure. */ export async function readPoolPriceEth(token: Address, poolId: Hex): Promise { try { const [sqrtPriceX96] = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.stateView, abi: stateViewAbi, functionName: "getSlot0", args: [poolId], }); // price(currency1 per currency0) = (sqrtPriceX96 / 2^96)^2 const ratio = (Number(sqrtPriceX96) / Q96) ** 2; // Orient so the result is ETH (WETH) per token. const ethPerToken = isTokenCurrency0(token) ? ratio : 1 / ratio; return ethPerToken > 0 ? ethPerToken : null; } catch { return null; } } ``` `sqrtPriceX96` is a Q64.96 fixed-point number. The `Number()`-based conversion above is fine for display; for high-precision accounting, compute the square in `BigInt` before converting. ## 6. Pool ID Derivation Prefer the `poolId` from `getTokenState` or the `TokenCreated` event. When you must derive it yourself, it's `keccak256(abi.encode(poolKey))` over the sorted key: ```typescript pool-id.ts theme={null} import { encodeAbiParameters, keccak256, type Address, type Hex } from "viem"; import { bagsPoolKey } from "./poolKey"; export function poolIdForToken(token: Address): Hex { const key = bagsPoolKey(token); return keccak256( encodeAbiParameters( [{ type: "address" }, { type: "address" }, { type: "uint24" }, { type: "int24" }, { type: "address" }], [key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks] ) ); } ``` The derivation must match the factory byte-for-byte or every derived `poolId` is wrong. Cross-check your derived value against the on-chain `poolId` from `getTokenState` for a known token, and prefer the on-chain value whenever it's available. ## Next steps * Trade against this state with the [Trade Tokens](/robinhood/trade-tokens) guide. * Read `claimableOf` and the fee-share breakdown in the [Claim Creator Fees](/robinhood/claim-fees) guide. * See the [Contracts Reference](/robinhood/contracts) for every read function and event. # Environment Setup Source: https://docs.bags.fm/robinhood/setup Set up viem clients, the Robinhood Chain definition, contract addresses, and ABIs before integrating Bags launching and trading. This guide sets up the shared building blocks — chain definition, read/write clients, addresses, and ABIs — used by every other Robinhood Chain guide. The examples use [viem](https://viem.sh), but the same calls translate directly to [ethers](https://docs.ethers.org) or any EVM library. ## Prerequisites Before starting, make sure you have: * Node.js 18+ and a TypeScript project. If you're starting fresh, follow the [TypeScript & Node.js Setup Guide](/how-to-guides/typescript-node-setup) for the base project, then install the EVM dependencies below. * Some ETH on Robinhood Chain (chain ID `4663`) in the wallet you'll sign with. * Read the [Robinhood Chain Overview](/robinhood/overview) for the token lifecycle and address book. No Bags API key is required for Robinhood Chain. You interact with the public smart contracts directly. ## 1. Install Dependencies ```bash theme={null} npm install viem dotenv ``` ## 2. Set Up Environment Variables Create a `.env` file. The public RPC works out of the box but is rate-limited; set a dedicated endpoint if you have one. ```bash theme={null} # .env ROBINHOOD_RPC_URL=https://rpc.mainnet.chain.robinhood.com # Required only for guides that send transactions (launch, trade, claim): PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE ``` Never commit your `.env` file. Add it to `.gitignore`. Treat your private key as a secret. ## 3. Define the Chain Save this as `chain.ts`. It defines Robinhood Chain and tunes viem for the chain's \~100 ms blocks. ```typescript chain.ts theme={null} import { defineChain } from "viem"; export const ROBINHOOD_CHAIN_ID = 4663; export const ROBINHOOD_RPC_URL = process.env.ROBINHOOD_RPC_URL || "https://rpc.mainnet.chain.robinhood.com"; export const robinhoodChain = defineChain({ id: ROBINHOOD_CHAIN_ID, name: "Robinhood Chain", nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [ROBINHOOD_RPC_URL] }, }, blockExplorers: { default: { name: "Blockscout", url: "https://robinhoodchain.blockscout.com" }, }, contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" }, }, }); ``` ## 4. Addresses & Protocol Constants Save this as `addresses.ts`. These are the protocol singletons and shared infrastructure (see the [full address book](/robinhood/overview#contract-address-book)). ```typescript addresses.ts theme={null} export const ROBINHOOD_LAUNCHPAD = { /** Launch entry point + on-chain registry (UUPS proxy — stable address). */ factory: "0xe8Cc4431adF8b5A847C113EF0c6af9043219Cb37", /** Stateless read aggregator — getTokenState(s), claimableOf. */ lens: "0xC82Db941dAf90B754aecb5F7D14c683dc608d595", /** Singleton v4 hook shared by all Bags pools. */ hook: "0x2380aBf72C17aABAb76480244759AC7E2932EEcC", /** Platform treasury (UUPS proxy, native ETH). */ vault: "0x4861446aa7fFd9e67a83cBbAcb1A4B70540B83Aa", /** Robinhood-MODIFIED UniversalRouter fork (custom swap struct). */ universalRouter: "0x8876789976dEcBfCbBbe364623C63652db8C0904", /** v4-periphery quoter (off-chain quoting only). */ v4Quoter: "0x8Dc178eFB8111BB0973Dd9d722ebeFF267c98F94", /** v4 pool state reads: getSlot0(poolId), getLiquidity(poolId). */ stateView: "0xF3334192D15450CdD385c8B70e03f9A6bD9E673b", /** v4 PoolManager singleton (Swap logs). */ poolManager: "0x8366a39CC670B4001A1121B8F6A443A643e40951", /** v4 PositionManager (migration LP mint — reads only). */ positionManager: "0x58daec3116aae6D93017bAAea7749052E8a04fA7", /** Canonical Permit2 — spender route for UniversalRouter ERC-20 inputs. */ permit2: "0x000000000022D473030F116dDEE9F6B43aC78BA3", /** aeWETH upgradeable proxy — WETH9-compatible interface. */ weth: "0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73", } as const; /** First protocol deploy block — lower bound for any log scan. */ export const ROBINHOOD_DEPLOY_BLOCK = 7887312n; /** Native ETH sentinel address. */ export const ROBINHOOD_NATIVE_TOKEN = "0x0000000000000000000000000000000000000000"; /** * Uniswap v4 PoolKey tuning for Bags pools. Must match the factory * byte-for-byte or derived poolIds are wrong. */ export const ROBINHOOD_POOL = { dynamicFeeFlag: 0x800000, tickSpacing: 60, } as const; /** * Fee model: 2% of the ETH/WETH leg on every trade. * Creator half (1%) goes to the token's fee claimers; protocol half (1%) is * split between the optional partner (at the launch-snapshotted partnerFeeBps, * read live from the factory/curve) and the vault. */ export const ROBINHOOD_FEES = { txFeeBps: 200, creatorHalfBps: 100, protocolHalfBps: 100, bpsDenominator: 10_000, } as const; /** Fixed supply per Bags token: 1,000,000,000 x 1e18. FDV = spot price x 1e9. */ export const ROBINHOOD_TOKEN_SUPPLY_WEI = 1_000_000_000_000000000000000000n; ``` ## 5. Create the Clients Save this as `clients.ts`. The **public client** is for reads and simulations; the **wallet client** signs and sends transactions. ```typescript clients.ts theme={null} import { createPublicClient, createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { robinhoodChain, ROBINHOOD_RPC_URL } from "./chain"; /** * Shared read client. `batch.multicall` coalesces parallel readContract calls * into single multicall3 requests — the main defense against public-RPC rate * limits. 500 ms polling suits the ~100 ms block time. */ export const publicClient = createPublicClient({ chain: robinhoodChain, transport: http(ROBINHOOD_RPC_URL, { batch: true }), batch: { multicall: true }, pollingInterval: 500, }); /** Write client. Only needed for launch / trade / claim guides. */ export function getWalletClient() { const pk = process.env.PRIVATE_KEY; if (!pk) throw new Error("PRIVATE_KEY is required to send transactions"); const account = privateKeyToAccount(pk as `0x${string}`); return createWalletClient({ account, chain: robinhoodChain, transport: http(ROBINHOOD_RPC_URL), }); } ``` In a browser dApp, replace `getWalletClient` with a wallet-backed client, e.g. `createWalletClient({ account, chain: robinhoodChain, transport: custom(window.ethereum) })`, and switch the wallet to chain `4663` before writing. ## 6. Get the ABIs The Bags contract ABIs are published in the public [`robinhood-abi-v2`](https://github.com/bagsfm/bags-idl/tree/main/robinhood-abi-v2) directory of the [`bagsfm/bags-idl`](https://github.com/bagsfm/bags-idl) repository. It contains eight JSON files: * `BagsFactory.json`, `BagsBondingCurve.json`, `BagsFeeShare.json`, `BagsLens.json`, `BagsToken.json`, `BagsV4Hook.json`, `BagsVault.json`, `BagsBeacon.json` Import each JSON as an ABI, for example: ```typescript abi/index.ts theme={null} import bagsFactory from "./BagsFactory.json"; import bagsBondingCurve from "./BagsBondingCurve.json"; import bagsFeeShare from "./BagsFeeShare.json"; import bagsLens from "./BagsLens.json"; import bagsToken from "./BagsToken.json"; import bagsV4Hook from "./BagsV4Hook.json"; import bagsVault from "./BagsVault.json"; export const bagsFactoryAbi = bagsFactory; export const bagsBondingCurveAbi = bagsBondingCurve; export const bagsFeeShareAbi = bagsFeeShare; export const bagsLensAbi = bagsLens; export const bagsTokenAbi = bagsToken; export const bagsV4HookAbi = bagsV4Hook; export const bagsVaultAbi = bagsVault; ``` Import the **full** ABIs including their custom `error` definitions — viem uses them to decode revert reasons into readable errors (e.g. `BagsBondingCurve_SlippageExceeded`). See the [Contracts Reference](/robinhood/contracts) for the error catalog. ### Periphery ABIs (hand-written) The Uniswap-style infrastructure (UniversalRouter, Permit2, WETH, V4Quoter, StateView) is **not** part of the Bags ABI export. The [Trade Tokens](/robinhood/trade-tokens) guide includes the minimal hand-written ABIs you need for post-migration swaps. The full definitions are also listed in the [Contracts Reference](/robinhood/contracts#periphery-abis). ## 7. Verify Your Setup Read a value from the factory to confirm everything is wired up. ```typescript verify.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsFactoryAbi } from "./abi"; async function main() { const [creationFee, tokenCount] = await Promise.all([ publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName: "creationFee", }), publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.factory, abi: bagsFactoryAbi, functionName: "allTokensLength", }), ]); console.log("Creation fee (wei):", creationFee.toString()); console.log("Tokens launched:", tokenCount.toString()); } main().catch(console.error); ``` ```bash theme={null} npx ts-node verify.ts ``` If you see the creation fee (default `0.02 ETH` = `20000000000000000` wei) and a token count printed, you're ready to move on. ## Next steps Create a token through the BagsFactory. Buy and sell across both phases. Query state and list tokens with BagsLens. Claim accrued fees from BagsFeeShare. # Trade Tokens Source: https://docs.bags.fm/robinhood/trade-tokens Buy and sell Bags tokens on Robinhood Chain: bonding-curve trades before graduation and Uniswap v4 swaps (via the modified UniversalRouter) after. In this guide, you'll trade Bags tokens on Robinhood Chain. Which venue you use depends on whether the token has **graduated**: * **Before graduation** — trade against the token's `BagsBondingCurve` (buy with native ETH, sell for native ETH). * **After graduation** — trade against the token/WETH Uniswap v4 pool through the Robinhood-modified UniversalRouter. Always determine the phase first, then route accordingly. ## Prerequisites Before starting, make sure you have: * Completed the [Environment Setup](/robinhood/setup). * Read the [Overview](/robinhood/overview) (fee model, lifecycle) and know the token address you want to trade. * ETH in your wallet for buys and gas. ## 1. Route by Migration Status Read the token's state and branch on `migrated`. The simplest source is `BagsLens` (see [Read State](/robinhood/read-state)): ```typescript theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { bagsLensAbi } from "./abi"; const state = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.lens, abi: bagsLensAbi, functionName: "getTokenState", args: [token], }); if (!state.exists) throw new Error("Not a Bags token"); if (state.migrated) { // Uniswap v4 pool — see "Trade after graduation" } else { // Bonding curve at state.curve — see "Trade on the bonding curve" } ``` A buy that completes the bonding curve graduates the token **in the same transaction**. After any curve buy, re-read `migrated` (or check for the `Migrated` event in the receipt) before the next trade so you don't send a curve call to a paused curve. ## 2. Slippage Helper Every trade below derives `minOut` from a **fresh** on-chain quote taken immediately before signing, with slippage applied. Use pure integer math: ```typescript slippage.ts theme={null} const BPS = 10_000n; /** amount * (10000 - slippageBps) / 10000, rounded down. Clamps bps to [0, 10000]. */ export function applySlippage(amount: bigint, slippageBps: number): bigint { const clamped = BigInt(Math.min(Math.max(Math.round(slippageBps), 0), 10_000)); const minOut = (amount * (BPS - clamped)) / BPS; if (minOut <= 0n) throw new Error("Amount too small to trade safely."); return minOut; } ``` Take the quote right before the swap — not from a stale polled value. Approvals and block time can move reserves enough to trip slippage otherwise. ## 3. Trade on the Bonding Curve Before graduation, trade directly against `state.curve`. ### Buy (ETH to token) Quote with `quoteBuy`, then call `buy` with native ETH as `value`: ```typescript curve-buy.ts theme={null} import { publicClient, getWalletClient } from "./clients"; import { bagsBondingCurveAbi } from "./abi"; import { applySlippage } from "./slippage"; import { parseEventLogs, type Address } from "viem"; export async function buyOnCurve(curve: Address, ethInWei: bigint, slippageBps: number) { const walletClient = getWalletClient(); // quoteBuy returns: tokensOut, feeQuote, netQuoteIn, grossUsed, refundQuote const [tokensOut, , , grossUsed, refundQuote] = await publicClient.readContract({ address: curve, abi: bagsBondingCurveAbi, functionName: "quoteBuy", args: [ethInWei], }); if (tokensOut <= 0n) throw new Error("Buy quote unavailable."); // refundQuote > 0 means this buy completes the curve: only `grossUsed` of your // ETH is spent, the rest is refunded, and the token graduates. if (refundQuote > 0n) { console.log(`This buy completes the curve. Only ${grossUsed} wei will be used; the rest is refunded.`); } const minTokensOut = applySlippage(tokensOut, slippageBps); const { request } = await publicClient.simulateContract({ account: walletClient.account, address: curve, abi: bagsBondingCurveAbi, functionName: "buy", args: [minTokensOut], value: ethInWei, }); const txHash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") throw new Error("Buy reverted."); const migrated = parseEventLogs({ abi: bagsBondingCurveAbi, logs: receipt.logs, eventName: "Migrated" }).length > 0; return { txHash, migrated }; } ``` `buyFor(recipient, minTokensOut)` is also available if you want the tokens delivered to a different address. ### Sell (token to ETH) Curve sells pull tokens via a plain ERC-20 `transferFrom`, so you must **approve the curve** first (a one-time `maxUint256` approval is convenient). Then quote with `quoteSell` and call `sell`: ```typescript curve-sell.ts theme={null} import { publicClient, getWalletClient } from "./clients"; import { bagsBondingCurveAbi, bagsTokenAbi } from "./abi"; import { applySlippage } from "./slippage"; import { maxUint256, type Address } from "viem"; export async function sellOnCurve(token: Address, curve: Address, tokensInWei: bigint, slippageBps: number) { const walletClient = getWalletClient(); const owner = walletClient.account.address; // Approve the curve once if the allowance is short (plain ERC-20 — NOT Permit2). const allowance = await publicClient.readContract({ address: token, abi: bagsTokenAbi, functionName: "allowance", args: [owner, curve], }); if (allowance < tokensInWei) { const { request } = await publicClient.simulateContract({ account: walletClient.account, address: token, abi: bagsTokenAbi, functionName: "approve", args: [curve, maxUint256], }); const approveTx = await walletClient.writeContract(request); await publicClient.waitForTransactionReceipt({ hash: approveTx }); } // Quote AFTER approval so minOut reflects current reserves. // quoteSell returns: quoteToSeller, feeQuote, grossQuoteOut const [quoteToSeller] = await publicClient.readContract({ address: curve, abi: bagsBondingCurveAbi, functionName: "quoteSell", args: [tokensInWei], }); if (quoteToSeller <= 0n) throw new Error("Sell quote unavailable."); const minQuoteOut = applySlippage(quoteToSeller, slippageBps); const { request } = await publicClient.simulateContract({ account: walletClient.account, address: curve, abi: bagsBondingCurveAbi, functionName: "sell", args: [tokensInWei, minQuoteOut], }); const txHash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") throw new Error("Sell reverted."); return { txHash }; } ``` Proceeds are paid as native ETH. `sellFor(recipient, tokensIn, minQuoteOut)` delivers proceeds to another address. `BagsToken` supports [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit`, so instead of a separate `approve` transaction you can have the seller sign a permit off-chain and submit the allowance gaslessly (or bundle permit + sell via multicall infrastructure) for a one-transaction sell UX. ## 4. Trade After Graduation (Uniswap v4) After migration, the token trades in a Uniswap v4 pool paired with **WETH**. Swaps go through the Robinhood-**modified** UniversalRouter. This router is a fork. Its v4 swap struct has an extra `uint256 minHopPriceX36` field (set it to `0`), so **stock Uniswap SDK calldata reverts** — you must encode the calldata manually as shown below. Only the router at `0x8876...0904` is correct. Key facts for the pool phase: * The pool pairs the token with **WETH**, not native ETH. To buy you need a sufficient **WETH** balance — wrap only the shortfall (the amount your wallet is missing), not the full input, since it may already hold some WETH. To receive native ETH after a sell you **unwrap WETH**. * The router spends your ERC-20 input through **Permit2**, so you must set up a two-leg Permit2 route once. * Swaps are **exact-in only** — the Bags hook reverts exact-out WETH by design. * Quote amounts from `V4Quoter` **already include the 2% hook fee**; apply slippage only, never subtract the fee again. ### 4a. Periphery ABIs These infrastructure contracts aren't part of the Bags ABI export. Save the minimal fragments as `abi/periphery.ts`: ```typescript abi/periphery.ts theme={null} export const universalRouterAbi = [ { type: "function", name: "execute", inputs: [ { name: "commands", type: "bytes" }, { name: "inputs", type: "bytes[]" }, { name: "deadline", type: "uint256" }, ], outputs: [], stateMutability: "payable", }, ] as const; export const permit2Abi = [ { type: "function", name: "approve", inputs: [ { name: "token", type: "address" }, { name: "spender", type: "address" }, { name: "amount", type: "uint160" }, { name: "expiration", type: "uint48" }, ], outputs: [], stateMutability: "nonpayable", }, { type: "function", name: "allowance", inputs: [ { name: "owner", type: "address" }, { name: "token", type: "address" }, { name: "spender", type: "address" }, ], outputs: [ { name: "amount", type: "uint160" }, { name: "expiration", type: "uint48" }, { name: "nonce", type: "uint48" }, ], stateMutability: "view", }, ] as const; export const v4QuoterAbi = [ { type: "function", name: "quoteExactInputSingle", inputs: [ { name: "params", type: "tuple", components: [ { name: "poolKey", type: "tuple", components: [ { name: "currency0", type: "address" }, { name: "currency1", type: "address" }, { name: "fee", type: "uint24" }, { name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" }, ], }, { name: "zeroForOne", type: "bool" }, { name: "exactAmount", type: "uint128" }, { name: "hookData", type: "bytes" }, ], }, ], outputs: [ { name: "amountOut", type: "uint256" }, { name: "gasEstimate", type: "uint256" }, ], stateMutability: "nonpayable", }, ] as const; /** aeWETH proxy — WETH9-compatible interface (never rely on bytecode/codehash). */ export const wethAbi = [ { type: "function", name: "deposit", inputs: [], outputs: [], stateMutability: "payable" }, { type: "function", name: "withdraw", inputs: [{ name: "amount", type: "uint256" }], outputs: [], stateMutability: "nonpayable" }, { type: "function", name: "approve", inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", }, { type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view", }, ] as const; ``` ### 4b. Pool key and swap direction The pool is identified by a v4 `PoolKey`. It must match the factory's derivation exactly. Save as `poolKey.ts`: ```typescript poolKey.ts theme={null} import { getAddress, type Address } from "viem"; import { ROBINHOOD_LAUNCHPAD, ROBINHOOD_POOL } from "./addresses"; /** True when the token sorts below WETH and is therefore currency0. */ export function isTokenCurrency0(token: Address): boolean { return BigInt(token) < BigInt(ROBINHOOD_LAUNCHPAD.weth); } /** The v4 PoolKey the factory creates for a Bags token (token/WETH, dynamic fee, Bags hook). */ export function bagsPoolKey(token: Address) { const tokenAddress = getAddress(token); const weth = getAddress(ROBINHOOD_LAUNCHPAD.weth); const tokenIs0 = isTokenCurrency0(tokenAddress); return { currency0: tokenIs0 ? tokenAddress : weth, currency1: tokenIs0 ? weth : tokenAddress, fee: ROBINHOOD_POOL.dynamicFeeFlag, tickSpacing: ROBINHOOD_POOL.tickSpacing, hooks: getAddress(ROBINHOOD_LAUNCHPAD.hook), }; } /** Buy = WETH -> token, sell = token -> WETH. zeroForOne is true when the INPUT is currency0. */ export function directionFor(token: Address, side: "buy" | "sell"): { zeroForOne: boolean } { const tokenIs0 = isTokenCurrency0(token); return { zeroForOne: side === "buy" ? !tokenIs0 : tokenIs0 }; } ``` ### 4c. Quote a pool swap `V4Quoter.quoteExactInputSingle` is **not** a view function — call it through `simulateContract`. The returned amount already accounts for the 2% hook fee. ```typescript pool-quote.ts theme={null} import { publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { v4QuoterAbi } from "./abi/periphery"; import { bagsPoolKey, directionFor } from "./poolKey"; import type { Address } from "viem"; export async function quotePoolExactIn(token: Address, side: "buy" | "sell", amountIn: bigint) { try { const { result } = await publicClient.simulateContract({ address: ROBINHOOD_LAUNCHPAD.v4Quoter, abi: v4QuoterAbi, functionName: "quoteExactInputSingle", args: [ { poolKey: bagsPoolKey(token), zeroForOne: directionFor(token, side).zeroForOne, exactAmount: amountIn, hookData: "0x", }, ], }); return result[0]; // amountOut } catch { return null; // no quote — treat as unavailable, never as 0 } } ``` ### 4d. Wrap ETH and set up the Permit2 route To buy, wrap the ETH you're about to spend; to swap any ERC-20 through the router, ensure the two-leg Permit2 route (`ERC-20 -> Permit2`, then `Permit2 -> router`). ```typescript permit2.ts theme={null} import { erc20Abi, maxUint160, maxUint256, type Address } from "viem"; import { publicClient, getWalletClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { permit2Abi, wethAbi } from "./abi/periphery"; const PERMIT2_EXPIRY_SECONDS = 30 * 24 * 3600; export async function wrapEth(amountWei: bigint) { const walletClient = getWalletClient(); const { request } = await publicClient.simulateContract({ account: walletClient.account, address: ROBINHOOD_LAUNCHPAD.weth, abi: wethAbi, functionName: "deposit", value: amountWei, }); const txHash = await walletClient.writeContract(request); await publicClient.waitForTransactionReceipt({ hash: txHash }); return txHash; } /** Ensure the router can pull `amountIn` of `inputToken` via Permit2. Runs at most two one-time approvals. */ export async function ensurePermit2Route(inputToken: Address, amountIn: bigint) { const walletClient = getWalletClient(); const owner = walletClient.account.address; const [erc20ToPermit2, permit2Allowance] = await Promise.all([ publicClient.readContract({ address: inputToken, abi: erc20Abi, functionName: "allowance", args: [owner, ROBINHOOD_LAUNCHPAD.permit2] }), publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.permit2, abi: permit2Abi, functionName: "allowance", args: [owner, inputToken, ROBINHOOD_LAUNCHPAD.universalRouter] }), ]); const [permit2Amount, permit2Expiration] = permit2Allowance; // Leg 1: ERC-20 -> Permit2 if (erc20ToPermit2 < amountIn) { const { request } = await publicClient.simulateContract({ account: walletClient.account, address: inputToken, abi: erc20Abi, functionName: "approve", args: [ROBINHOOD_LAUNCHPAD.permit2, maxUint256], }); await publicClient.waitForTransactionReceipt({ hash: await walletClient.writeContract(request) }); } // Leg 2: Permit2 -> router (amount is uint160; expiration is uint48) const nowSec = Math.floor(Date.now() / 1000); if (permit2Amount < amountIn || permit2Expiration <= nowSec) { const { request } = await publicClient.simulateContract({ account: walletClient.account, address: ROBINHOOD_LAUNCHPAD.permit2, abi: permit2Abi, functionName: "approve", args: [inputToken, ROBINHOOD_LAUNCHPAD.universalRouter, maxUint160, nowSec + PERMIT2_EXPIRY_SECONDS], }); await publicClient.waitForTransactionReceipt({ hash: await walletClient.writeContract(request) }); } } ``` ### 4e. Execute the swap Encode the modified router calldata manually: command `V4_SWAP (0x10)`, actions `[SWAP_EXACT_IN_SINGLE (0x06), SETTLE_ALL (0x0c), TAKE_ALL (0x0f)]`, with the extra `minHopPriceX36` set to `0`. ```typescript pool-swap.ts theme={null} import { encodeAbiParameters, encodePacked, type Address, type Hex } from "viem"; import { publicClient, getWalletClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { universalRouterAbi } from "./abi/periphery"; import { bagsPoolKey, directionFor } from "./poolKey"; const V4_SWAP_COMMAND: Hex = "0x10"; const V4_ACTIONS = encodePacked(["uint8", "uint8", "uint8"], [0x06, 0x0c, 0x0f]); const ROUTER_DEADLINE_SECONDS = 300; /** Post-migration exact-in swap. Input is ALWAYS an ERC-20 (WETH for buys, the token for sells). */ export async function swapV4ExactIn(token: Address, side: "buy" | "sell", amountIn: bigint, minAmountOut: bigint) { const walletClient = getWalletClient(); const poolKey = bagsPoolKey(token); const { zeroForOne } = directionFor(token, side); const inputCurrency: Address = side === "buy" ? ROBINHOOD_LAUNCHPAD.weth : token; const outputCurrency: Address = side === "buy" ? token : ROBINHOOD_LAUNCHPAD.weth; const swapParams = encodeAbiParameters( [ { type: "tuple", components: [ { name: "poolKey", type: "tuple", components: [ { name: "currency0", type: "address" }, { name: "currency1", type: "address" }, { name: "fee", type: "uint24" }, { name: "tickSpacing", type: "int24" }, { name: "hooks", type: "address" }, ], }, { name: "zeroForOne", type: "bool" }, { name: "amountIn", type: "uint128" }, { name: "amountOutMinimum", type: "uint128" }, // Robinhood-only field vs vanilla Uniswap — always 0 (disabled). { name: "minHopPriceX36", type: "uint256" }, { name: "hookData", type: "bytes" }, ], }, ], [{ poolKey, zeroForOne, amountIn, amountOutMinimum: minAmountOut, minHopPriceX36: 0n, hookData: "0x" }] ); const settleAll = encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [inputCurrency, amountIn]); const takeAll = encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [outputCurrency, minAmountOut]); const routerInput = encodeAbiParameters([{ type: "bytes" }, { type: "bytes[]" }], [V4_ACTIONS, [swapParams, settleAll, takeAll]]); // Deadline uses a timestamp — block numbers are unreliable on this Orbit chain. const deadline = BigInt(Math.floor(Date.now() / 1000) + ROUTER_DEADLINE_SECONDS); const { request } = await publicClient.simulateContract({ account: walletClient.account, address: ROBINHOOD_LAUNCHPAD.universalRouter, abi: universalRouterAbi, functionName: "execute", args: [V4_SWAP_COMMAND, [routerInput], deadline], }); const txHash = await walletClient.writeContract(request); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); if (receipt.status !== "success") throw new Error("Swap reverted."); return { txHash }; } ``` ### 4f. Full pool buy and sell Compose the helpers. A **buy** wraps ETH, ensures the WETH route, quotes, then swaps. A **sell** ensures the token route, quotes, swaps, then unwraps the WETH proceeds back to native ETH. ```typescript pool-trade.ts theme={null} import { getWalletClient, publicClient } from "./clients"; import { ROBINHOOD_LAUNCHPAD } from "./addresses"; import { wethAbi } from "./abi/periphery"; import { applySlippage } from "./slippage"; import { quotePoolExactIn } from "./pool-quote"; import { swapV4ExactIn } from "./pool-swap"; import { wrapEth, ensurePermit2Route } from "./permit2"; import type { Address } from "viem"; export async function buyPool(token: Address, ethInWei: bigint, slippageBps: number) { const walletClient = getWalletClient(); const owner = walletClient.account.address; // Wrap only the shortfall — the wallet may already hold some WETH. Wrapping the // full amount unconditionally reverts when native ETH is below ethInWei. const wethBalance = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.weth, abi: wethAbi, functionName: "balanceOf", args: [owner], }); const wrapAmount = ethInWei > wethBalance ? ethInWei - wethBalance : 0n; if (wrapAmount > 0n) await wrapEth(wrapAmount); await ensurePermit2Route(ROBINHOOD_LAUNCHPAD.weth, ethInWei); const amountOut = await quotePoolExactIn(token, "buy", ethInWei); if (!amountOut || amountOut <= 0n) throw new Error("Swap quote unavailable."); return swapV4ExactIn(token, "buy", ethInWei, applySlippage(amountOut, slippageBps)); } export async function sellPool(token: Address, tokensInWei: bigint, slippageBps: number) { const walletClient = getWalletClient(); const owner = walletClient.account.address; await ensurePermit2Route(token, tokensInWei); const amountOut = await quotePoolExactIn(token, "sell", tokensInWei); if (!amountOut || amountOut <= 0n) throw new Error("Swap quote unavailable."); // Snapshot WETH before the swap so we only unwrap the proceeds. const wethBefore = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.weth, abi: wethAbi, functionName: "balanceOf", args: [owner], }); const swap = await swapV4ExactIn(token, "sell", tokensInWei, applySlippage(amountOut, slippageBps)); // Unwrap the WETH gained into native ETH (optional convenience step). const wethAfter = await publicClient.readContract({ address: ROBINHOOD_LAUNCHPAD.weth, abi: wethAbi, functionName: "balanceOf", args: [owner], }); const delta = wethAfter - wethBefore; if (delta > 0n) { const { request } = await publicClient.simulateContract({ account: walletClient.account, address: ROBINHOOD_LAUNCHPAD.weth, abi: wethAbi, functionName: "withdraw", args: [delta], }); await publicClient.waitForTransactionReceipt({ hash: await walletClient.writeContract(request) }); } return swap; } ``` The unwrap is a secondary convenience transaction — the sale itself is already final once the swap confirms. If unwrapping fails, your proceeds are simply held as WETH and can be unwrapped or reused later. ## 5. Understanding Fees and Quotes * The 2% fee applies in **both** phases. On the curve, `quoteBuy`/`quoteSell` return fee-adjusted amounts (`tokensOut` / `quoteToSeller` are what you actually receive). In the pool, `V4Quoter` output already includes the hook fee. * `quoteBuy` returns `refundQuote > 0` when a buy would cross graduation — only `grossUsed` is spent and the rest is refunded. Surface this to users. * Distinguish a **failed** quote (RPC error / revert) from a **zero** quote. Treat a failure as "no quote" and disable trading; never coerce it to `0` (that would send an unprotected `minOut = 0`). ## Troubleshooting * **`BagsBondingCurve_SlippageExceeded(minExpected, actualOut)`** — reserves moved between quote and execution. Re-quote immediately before signing and/or raise slippage. * **`BagsBondingCurve_AlreadyMigrated` / `EnforcedPause`** — the token graduated; switch to the pool path. * **Pool swap reverts with stock SDK calldata** — you must include the `minHopPriceX36` field (set `0`) and encode manually, as in section 4e. The correct router is `0x8876...0904`. * **Router pulls nothing / transfer fails** — the Permit2 route isn't set up. Run `ensurePermit2Route` for the input token first (WETH for buys, the token for sells) and wait for the approvals to confirm. * **Exact-out WETH reverts (`BagsV4Hook_ExactOutputWETHSpecifiedUnsupported`)** — unsupported by design. Always swap exact-in. For the full function, event, and error catalog, see the [Contracts Reference](/robinhood/contracts).