{ const params = new URLSearchParams({ chainId: '8453', sellToken: USDC, buyToken: WETH, sellAmount: sellAmount.toString(), taker: user, slippageBps: '50', }); const response = await fetch( `https://api.0x.org/swap/allowance-holder/quote?${params}`, { headers: { '0x-api-key': required('ZERO_EX_API_KEY'), '0x-version': 'v2' } }, ); if (!response.ok) throw new Error(`0x quote failed: ${response.status} ${await response.text()}`); return (await response.json()) as Quote; } let quote = await getQuote(); if (!quote.liquidityAvailable) throw new Error('No route is currently available'); if (quote.issues.balance) throw new Error('Insufficient USDC balance'); if (quote.issues.allowance) { const approval = await publicClient.simulateContract({ account: user, address: USDC, abi: parseAbi(['function approve(address,uint256) returns (bool)']), functionName: 'approve', args: [quote.issues.allowance.spender, sellAmount], }); await publicClient.waitForTransactionReceipt({ hash: await walletClient.writeContract(approval.request), }); quote = await getQuote(); } if (quote.issues.allowance) throw new Error('Token allowance is still insufficient'); if (quote.issues.simulationIncomplete) throw new Error('0x could not complete its simulation'); if (!quote.transaction) throw new Error('Quote did not include transaction data'); const transaction = { account: walletClient.account, to: quote.transaction.to, data: quote.transaction.data, value: BigInt(quote.transaction.value ?? '0'), }; await publicClient.call({ ...transaction, account: user }); const hash = await walletClient.sendTransaction(transaction); await publicClient.waitForTransactionReceipt({ hash }); console.log(`Swap confirmed: ${hash}`); console.log(`Minimum WETH output: ${formatUnits(BigInt(quote.minBuyAmount), 18)} WETH`); ``` ## 0x Documentation## See Also Follow the full price, allowance, quote, and submission flow. Review every request parameter and response field. Approve the correct spender and avoid unsafe approvals. # List Tokenized Stocks Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/integrate-defi/list-tokenized-stocks Let users trade Coinbase-issued tokenized stocks on your app. Tokenized Stocks on Base are built on the Base-native token standard, [B20](/build-on-base/issue-rwa/create-an-asset-token). B20 is an extension of the ERC-20 token standard and is intended to be asset-agnostic, with tokenized stocks being one of several possible use cases. Product-specific details about Tokenized Stocks on Base can be found within [coinbase.com/tokenize](https://coinbase.com/tokenize). ## Token Information and Listings Tokenized stocks launch similarly to any ERC-20 token: name, symbol, and icon come from the usual sources, along with a few notable B20 specifics. Standard ERC-20 methods and events are supported natively. * **Onchain:** [`name`](/specifications/b20/reference/interfaces/ib20/name), [`symbol`](/specifications/b20/reference/interfaces/ib20/symbol), [`decimals`](/specifications/b20/reference/interfaces/ib20/decimals), and [`contractURI`](/specifications/b20/reference/interfaces/ib20/contract-uri) ([ERC-7572](https://eips.ethereum.org/EIPS/eip-7572) metadata). * **Offchain:** logos are also available from the canonical token list and market-data aggregators. B20 specifics: * Tokens should be identified by address rather than ticker or symbol. Metadata is mutable onchain and should be indexed accordingly. * Discover new tokens by watching the [`B20Created`](/specifications/b20/reference/interfaces/ib20-factory) event. ## B20 Token Features ### Multipliers An underlying real-world asset may undergo events that change the redemption ratio of a B20 token. For tokenized stocks, these events include dividends or stock splits, which must be passed along to the holder of the B20 tokenized equity onchain. The [multiplier](/specifications/b20/reference/interfaces/ib20-asset/multiplier) variable instantly updates the redemption ratio of a B20 token based on any corporate actions that occur. Let users supply USDC to a money market. Open a collateralized loan against WETH. Route deposits into yield-bearing vaults. One B20 token does not permanently equal one share. Always apply the current multiplier when converting between token units and the number of underlying shares. Asset-level events are reflected by updating the multiplier. For example, after a dividend on a tokenized equity, the multiplier increases to `1.02`, representing that 1 B20 token is redeemable for 1.02 shares of the equity. At this time, for tokenized stocks, cash dividends are converted to shares of the underlying equity and reflected via a multiplier update rather than distributed as cash to the B20 holder. This allows B20 holder balances to automatically reflect corporate actions without changing their balance of the B20 token. Multiplier updates come in two forms: * **Scheduled (ERC-8056):** set a future-dated change with [`updateUIMultiplier`](/specifications/b20/reference/interfaces/ib20-asset/update-ui-multiplier), which gives advance onchain notice. Use this for routine corporate actions. * **Instant (deprecated):** [`updateMultiplier`](/specifications/b20/reference/interfaces/ib20-asset/update-multiplier) applies immediately and clears any pending scheduled change. It is a retained emergency failsafe; prefer the scheduled path. * **Cancellation:** clear a pending scheduled update with [`cancelUIMultiplierUpdate`](/specifications/b20/reference/interfaces/ib20-asset/cancel-ui-multiplier-update). The B20 contract provides several helper functions for common calculations, where `raw` is the number of B20 units and `scaled` is the quantity of stocks redeemable: | Function | Description | | --------------------------------------------------------------------------------------------------- | ---------------------------- | | [`scaledBalanceOf(account)`](/specifications/b20/reference/interfaces/ib20-asset/scaled-balance-of) | Raw balance × multiplier | | [`toScaledBalance(raw)`](/specifications/b20/reference/interfaces/ib20-asset/to-scaled-balance) | Convert raw amount to scaled | | [`toRawBalance(scaled)`](/specifications/b20/reference/interfaces/ib20-asset/to-raw-balance) | Convert scaled amount to raw | ### Policies To comply with any regulatory requirements that apply to a particular asset, [policies](/specifications/b20/reference/interfaces/i-policy-registry) that manage allowlists and blocklists may be implemented. Policies determine whether a transfer is allowed or rejected. The B20 contract provides the [`isAuthorized(policyID, account)`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) function, which you can use to determine whether a specific account is allowed to transfer funds.The standard [`approve()`](/specifications/b20/reference/interfaces/ib20/approve) function is not policy gated. Checking whether a quantity of funds is approved to be transferred does not guarantee that the funds aren't blocked by a policy. ### Pauses Onchain [pauses](/specifications/b20/reference/interfaces/ib20/pause) are unlikely, but be aware that B20s allow specific functions within a contract to be paused. Monitor these to maintain an accurate understanding of whether funds can be transferred at a given point in time. ### Announcements Sensitive operations are wrapped in onchain [announcement events](/specifications/b20/reference/interfaces/ib20-asset/announce): [`announce`](/specifications/b20/reference/interfaces/ib20-asset/announce) emits [`Announcement (id, description, uri)`](/specifications/b20/reference/interfaces/ib20-asset), then [`EndAnnouncement`](/specifications/b20/reference/interfaces/ib20-asset). Integrators index these to catch corporate actions as they execute. Two design points: * Announcements can be atomically bundled with the token change they describe (for example, the multiplier update for a stock split), keeping onchain records clean. * Descriptions are intentionally human-readable onchain to support public reporting requirements. **Admin actions:** admin operations and `updateMultiplier` ([`OPERATOR_ROLE`](/specifications/b20/reference/interfaces/ib20-asset/operator-role)) execute immediately when the role holder calls; the B20 standard has no built-in timelock. The `Announcement` and `EndAnnouncement` events are public notice, not an enforced delay. Any timelock or multisig is applied by the issuer at the governance layer. ### Extra Metadata Issuers can store arbitrary key/value data onchain via [`extraMetadata(key)`](/specifications/b20/reference/interfaces/ib20-asset/extra-metadata) (for example, security identifiers such as ISIN and CUSIP). ### Name and Symbol Name and symbol are updatable onchain ([`updateName`](/specifications/b20/reference/interfaces/ib20/update-name), [`updateSymbol`](/specifications/b20/reference/interfaces/ib20/update-symbol)), so the token can track offchain changes to the underlying without redeploying. ### Memos [`transferWithMemo`](/specifications/b20/reference/interfaces/ib20/transfer-with-memo) and [`transferFromWithMemo`](/specifications/b20/reference/interfaces/ib20/transfer-from-with-memo) attach a `bytes32` reference to an individual transfer (emitted as a `Memo` event), for annotating transfers with offchain data for reconciliation and reporting. ### Supply Cap An optional supply cap bounds total supply, mitigating over-minting from operational errors or a compromise. ## Compliance Holding and trading on the secondary market is permissionless. KYC only happens during mint and redeem flows taken by Authorized Participants (APs). * Onchain policies can block specific addresses, such as sanctioned addresses; a blocked transfer reverts (see [Policies](#policies) above). * Minting and redeeming the underlying shares is a separate, restricted issuer flow, as it is restricted to APs. **Security and audits:** tokenized stocks are B20 native precompiles, not separately deployed contracts, so there is no per-asset contract and no per-address verified contract on Basescan (precompiles hold no bytecode). B20 shipped in Base's Beryl upgrade on code audited by Base and Spearbit, with ongoing Cantina (smart contract) and HackerOne (offchain and infrastructure) bug-bounty coverage. Every token shares the same audited implementation. ## Price Feeds A tokenized stock's price is available from both onchain and offchain sources. In every case, the price is derived from the same relationship: the underlying equity's market price scaled by the token's multiplier. ```text Token price formula theme={null} Token Price = Underlying Equity Market Price × Multiplier ``` The multiplier is sourced per token and is WAD-scaled (a fixed-point number with 18 decimals). To get the real factor, divide the onchain value by that scale; call [`WAD_PRECISION()`](/specifications/b20/reference/interfaces/ib20-asset/wad-precision) to read the scale (it returns `1e18`). ### Onchain Chainlink is the onchain price option at launch. Each tokenized equity has a Chainlink feed that runs 24/5, holds the last close on weekends and holidays, and freezes during corporate actions. Feeds implement the standard Chainlink V3 aggregator interface and are read through the proxy, exactly like a crypto price feed; read the latest value with `latestRoundData()`. Unlike standard market-rate feeds, Coinbase feeds report **Total Return Values** rather than raw equity prices, so the price reflects the underlying's total return including corporate-action adjustments. Chainlink uses this same approach for other tokenized-equity issuers, including [Ondo and Robinhood](https://docs.chain.link/data-feeds/tokenized-equity-feeds/providers). The underlying price is sourced from Chainlink's equity price feeds (traditional market data), not from onchain or DEX trading of the token; the token's DEX price does not feed the oracle. The feed reads the multiplier and a pause flag from Coinbase's onchain oracle registry, a single contract (separate from the tokens) that returns both values for a token in one call: * **Normal (`paused = false`):** the feed publishes underlying price × multiplier. * **Paused (`paused = true`):** the feed stops publishing and holds the last known good value.During market hours the feed updates on a 0.5% price deviation or at least every 24 hours (its heartbeat). Off-hours (nights, weekends, holidays, and corporate-action pauses) it stops updating and holds the last value, so `updatedAt` stops advancing while the contract stays callable. Always read `updatedAt` and apply staleness bounds before relying on the price; never settle or liquidate against a frozen feed. During a corporate action: * Mint and redeem pause offchain, but the token is not paused onchain, so transfers are not blocked. * The feed freezes (the registry pause flag is set) and its price goes stale. * Because the feed is total-return, there is no price discontinuity: the multiplier and underlying price move in opposite directions and cancel (a 10:1 split drops the price \~10x and raises the multiplier \~10x). * **Fail-safe:** the feed resumes only after Coinbase confirms the underlying price and multiplier both reflect the new values. If one updates before the other, the feed stays frozen at the pre-pause price rather than publishing a half-applied value. **Chainlink data feeds on Base.** Read each via `latestRoundData()` on the proxy address below. All feeds return 8 decimals, cover US equities (24/5 market hours), and update on a 0.5% price deviation or a 24-hour heartbeat. Values are total-return; apply the pause and staleness handling above. | Feed | Address | | -------------- | -------------------------------------------- | | Coinbase AAPL | `0x787f13dEa48Db0897CbCDD985de77809D837F988` | | Coinbase AMZN | `0x06A8E4b3aBB3B7543d8396FB2B763d22820cB295` | | Coinbase COIN | `0x408e44f504A7371a345F03a73dDC96A4b48e8aa7` | | Coinbase CRCL | `0x0231cF2635D1E17bB5c2462cc7504Ba1fBd61f33` | | Coinbase GOOGL | `0x5bF49E0ffA937CE2FfF033c739aD7C634c4D34F2` | | Coinbase INTC | `0xAB657C39bac0D5886250D70849e2E3E008F2EECB` | | Coinbase META | `0x6526aE6797A76123638b863AeE4dD27Ba4E4b27D` | | Coinbase MSFT | `0xeB10A6c9aa7E537aEd766C08c35Dae35B321b18c` | | Coinbase MSTR | `0xB3cE282CD188b35DA0E38D8Bc7d58e33173D202a` | | Coinbase NVDA | `0x04689a41629776563E6822F76f2e57D148d28513` | | Coinbase SNDK | `0x388b0dC46C0Fb05A74BeE0994fa5b02c6Fcca2eA` | | Coinbase SPCX | `0x6A634B235903C4ad6376892180d6fF8612e3Fa68` | | Coinbase TSLA | `0xFaf869185383a24F8cb00e27BdA6b63B9905DCb4` | ### Offchain Offchain price data can be sourced from providers such as CoinGecko, CoinMarketCap, or RWA. These aggregators track the token's live market price from the DEXs where the B20 trades, which runs 24/7 whenever the secondary market is active. Two common patterns are used to determine value: * Directly reading the token's market price from a provider that tracks the B20 asset. * Reading the underlying reference price and applying the multiplier calculation manually. ### Historical Data For historical OHLC and time-series data, use market-data providers or query Chainlink round history by `roundId`. Since prices are total-return (multiplier-adjusted), reconstruct the series consistently by applying the multiplier history ([`MultiplierUpdated`](/specifications/b20/reference/interfaces/ib20-asset) events, emitted by both scheduled and instant multiplier changes) if starting from raw share prices. ## Contract Addresses | Ticker | Contract address | | ---------------- | -------------------------------------------- | | Onchain Registry | `0x3f3E8cf41cdd3b1D118c16471aB0113DfDDd5CaD` | | AAPLc | `0xb200000000000000000000C2e324d24d7eEcd1fb` | | AMZNc | `0xb200000000000000000000d9192b6B456483C2E8` | | COINc | `0xb200000000000000000000c85a31389D71F3ecfb` | | CRCLc | `0xB20000000000000000000019f6E7C675b73C2e4D` | | GOOGLc | `0xb2000000000000000000002D0BA3164cc74f58B7` | | INTCc | `0xB2000000000000000000004AFF16039bA04bdFBc` | | METAc | `0xb2000000000000000000008bC8786B856E61707C` | | MSFTc | `0xB200000000000000000000Ab99cFa739E253872B` | | MSTRc | `0xb2000000000000000000004884b426556b92883d` | | NVDAc | `0xb20000000000000000000078ee7ce2fE4908108C` | | SNDKc | `0xb200000000000000000000397293Cb8cda9a10c5` | | SPCXc | `0xb2000000000000000000007b9fcbd005511aCBd5` | | TSLAc | `0xb2000000000000000000001e800a7f5189430cD0` | ## Additional Resources * [B20 Standard](/specifications/b20/specification-overview) * [Base Standard Library](https://github.com/base/base-std/tree/main) ## DisclaimerCoinbase tokenized stocks are only available to persons in eligible jurisdictions outside of the U.S.
Inclusion of any third-party protocol or venue is for developer reference only and is not an endorsement, partnership, or warranty. Confirm addresses, feeds, and the token list against official sources before integrating.
Base is open-source, permissionless blockchain infrastructure. Each B20 token is deployed and configured by its issuer, who sets and controls all token parameters and administrative permissions; Base does not configure, administer, or control tokens deployed on the protocol.
# Announce a Distribution Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/announce-a-distribution Wrap a holder-impacting action with an onchain disclosure using the B20 Asset announce function. Bracket a holder-impacting action with an onchain description and supporting URI. `announce` emits `Announcement` before the inner calls and `EndAnnouncement` after, giving indexers a reliable bracket for every announced change.Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How Announcements Work `announce` takes `internalCalls`, a single-use `id`, a `description`, and a `uri`. * **`internalCalls`**: ABI-encoded calldata blobs targeting this asset. Each blob must be at least 4 bytes. Pass an empty array for a notice with no onchain effect. * **`id`**: chosen by the caller and consumed on success. Reuse reverts `AnnouncementIdAlreadyUsed`. After success, `isAnnouncementIdUsed(id)` returns `true`. * **`description` and `uri`**: operator-supplied strings. The asset does not verify them. The asset emits `Announcement(caller, id, description, uri)`, runs the inner calls atomically, then emits `EndAnnouncement(id)`. If any inner call fails, the whole transaction reverts. Nesting `announce` inside an inner call reverts `AnnouncementInProgress`. The caller must hold `OPERATOR_ROLE`. Any other caller reverts `AccessControlUnauthorizedAccount`. Inner calls keep their own role gates. If an inner call needs `MINT_ROLE` or `BURN_ROLE`, grant those to the operator as well. ## Announce and Distribute the Stock Dividend```typescript TypeScript lines wrap expandable highlight={9,13} theme={null} import { encodeFunctionData, parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { assetAbi } from "../abi.js"; import { sendContract } from "../write.js"; export async function announceStockDividend(token: Address, holders: Address[]) { const mint = encodeFunctionData({ abi: assetAbi, functionName: "batchMint", args: [holders, [parseUnits("30", 6), parseUnits("20", 6)]], }); const id = `dividend-${Date.now()}`; await sendContract({ address: token, abi: assetAbi, functionName: "announce", args: [[mint], id, "Five-percent stock dividend", "https://example.com/actions/dividend"], }); const used = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "isAnnouncementIdUsed", args: [id] }); if (!used) throw new Error("Announcement was not recorded"); } ``` ```solidity Solidity lines wrap expandable highlight={3,4} theme={null} function announceDividend(address token, address[] memory recipients, uint256[] memory amounts) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.batchMint, (recipients, amounts)); IB20Asset(token).announce( calls, "2026-stock-dividend-01", "Five-percent stock dividend", "https://example.com/announcements/2026-01" ); require(IB20Asset(token).isAnnouncementIdUsed("2026-stock-dividend-01"), "announcement missing"); } ``` The operator needs `OPERATOR_ROLE` and `MINT_ROLE`. Recipients must pass `MINT_RECEIVER_POLICY`. `MINT` must not be paused. On success the asset emits `Announcement`, then one `Transfer(address(0), recipient, amount)` per recipient, then `EndAnnouncement`.On success, the unique announcement `id` is marked used and the batch mint executes between `Announcement` and `EndAnnouncement`. This example issues additional shares. It does not distribute a cash dividend. A reinvested dividend that only rescales displayed balances is a multiplier update, covered below and in [Apply a Multiplier](/build-on-base/issue-rwa/apply-a-multiplier). ## Announce Other Changes The same bracket discloses any operator-driven change. Each scenario below lists the roles the operator needs and the events the asset emits. ### Multiplier Update Wrap `updateUIMultiplier(newMultiplier, effectiveAt)`. A 2-for-1 split uses `2e18`. A reverse split uses a value below `1e18`. The operator needs `OPERATOR_ROLE` only. ```solidity Announce a multiplier update lines wrap expandable highlight={3} theme={null} function announceMultiplierUpdate(address token, uint256 newMultiplier, uint256 effectiveAt) public { bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); IB20Asset(token).announce( calls, "2026-multiplier-01", "2-for-1 multiplier update", "https://example.com/announcements/2026-multiplier-01" ); } ``` On success the asset emits `Announcement`, `UIMultiplierUpdated`, then `EndAnnouncement`. `UIMultiplierUpdated` means the schedule was recorded, not that the multiplier is already active. See [Apply a Multiplier](/build-on-base/issue-rwa/apply-a-multiplier) for the full schedule, cancel, and override flow. To replace a live pending update, cancel and reschedule in one `announce`: ```solidity Cancel and reschedule in one announcement wrap theme={null} bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (newMultiplier, effectiveAt)); IB20Asset(token).announce(calls, id, description, uri); ``` ### Treasury Burn Wrap `burnWithMemo(amount, memo)`. The call burns the operator's own balance. The operator needs `OPERATOR_ROLE` and `BURN_ROLE`. `BURN` must not be paused. ```solidity Announce a treasury burn wrap theme={null} bytes[] memory calls = new bytes[](1); calls[0] = abi.encodeCall(IB20Asset.burnWithMemo, (amount, memo)); IB20Asset(token).announce(calls, id, "Treasury burn", uri); ``` On success the asset emits `Announcement`, `Transfer(operator, address(0), amount)`, `Memo`, then `EndAnnouncement`. `totalSupply` decreases. ### Notice With No Onchain Effect Pass an empty `internalCalls` array. The operator needs `OPERATOR_ROLE` only. The asset emits `Announcement` then `EndAnnouncement` with nothing between them. The `id` is still consumed. ```solidity Notice only theme={null} IB20Asset(token).announce(new bytes[](0), id, description, uri); ``` ## Common Errors | Error | Cause | Fix | | --------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------- | | `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller does not hold `OPERATOR_ROLE`. | Grant `OPERATOR_ROLE` to the operator. | | `AnnouncementIdAlreadyUsed(id)` | A prior successful `announce` consumed `id`. | Choose a new single-use `id`. | | `InternalCallMalformed(call)` | An inner-call blob is shorter than 4 bytes. | Pass ABI-encoded calldata that includes a selector. | | `AnnouncementInProgress()` | An inner call targeted `announce`. | Do not nest `announce`. | | `InternalCallFailed(call)` | An inner call reverted. The reason is not bubbled. | Replay `call` directly to see the underlying error, then fix the cause. | Typical inner causes of `InternalCallFailed`: missing `MINT_ROLE` or `BURN_ROLE`, paused `MINT` or `BURN`, `UIMultiplierUpdateExists`, `PolicyForbids`, `SupplyCapExceeded`, `InsufficientBalance`. A Solidity `Panic` (for example overflow) propagates raw and is not wrapped as `InternalCallFailed`. ## See Also# Apply a Multiplier Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/apply-a-multiplier Schedule a B20 Asset multiplier update so displayed share counts change at a future time without rewriting raw balances. A stock split is one example. Schedule a 2-for-1 stock split by calling `updateUIMultiplier(2e18, effectiveAt)` on a B20 Asset. Raw `balanceOf`, `totalSupply`, and transfer amounts stay unchanged. Wallets and indexers read the post-split share count through `balanceOfUI` and related views once `effectiveAt` passes. Mint asset units to holders. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. The routine path is `updateUIMultiplier`. The deprecated `updateMultiplier` applies a value immediately and clears any pending update. Use it only as an emergency override. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How the Multiplier Works Raw `balanceOf` is unchanged by a split. The multiplier changes only the derived UI view: | Read | Formula | | -------------------------------------- | -------------------------------------------- | | `uiMultiplier()` | Effective multiplier at `block.timestamp` | | `balanceOfUI(account)` | `balanceOf(account) * uiMultiplier() / 1e18` | | `totalSupplyUI()` | `totalSupply() * uiMultiplier() / 1e18` | | `toUIAmount(raw)` / `fromUIAmount(ui)` | Convert at the effective multiplier | The multiplier is an 18-decimal WAD: `1e18` is `1.0`. A 2-for-1 split uses `2e18`. A 1-for-2 reverse split uses `5e17`. Integer division rounds down, so the rounding loss is confined to the scaled view and is at most one unit of the scaled amount. The raw-side difference on a round trip through `fromUIAmount` can be larger when the multiplier is below `1e18`. Prefer 18 decimals for stock tokens to keep that effect small. ## Schedule and Verify the Split Multiplier Only an account holding `OPERATOR_ROLE` may call `updateUIMultiplier`, `cancelUIMultiplierUpdate`, or `updateMultiplier`. Any other caller reverts `AccessControlUnauthorizedAccount`. The token created in [Create an Asset Token](/build-on-base/issue-rwa/create-an-asset-token) grants `OPERATOR_ROLE` to the deployer.```typescript TypeScript lines wrap expandable highlight={8} theme={null} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { assetAbi } from "../abi.js"; import { sendContract } from "../write.js"; export async function scheduleTwoForOneSplit(token: Address) { const effectiveAt = BigInt(Math.floor(Date.now() / 1000) + 86_400); await sendContract({ address: token, abi: assetAbi, functionName: "updateUIMultiplier", args: [2n * 10n ** 18n, effectiveAt] }); const pending = await publicClient.readContract({ address: token, abi: assetAbi, functionName: "newUIMultiplier" }); if (pending !== 2n * 10n ** 18n) throw new Error("Split was not scheduled"); return effectiveAt; } ``` ```solidity Solidity lines wrap expandable highlight={3} theme={null} function scheduleSplit(address token) public returns (uint256 effectiveAt) { effectiveAt = block.timestamp + 1 days; IB20Asset(token).updateUIMultiplier(2e18, effectiveAt); require(IB20Asset(token).newUIMultiplier() == 2e18, "split not scheduled"); require(IB20Asset(token).effectiveAt() == effectiveAt, "effectiveAt mismatch"); } ``` ```bash CLI lines wrap expandable highlight={2} theme={null} EFFECTIVE_AT=$(( $(date +%s) + 86400 )) base-cast send "$TOKEN_ADDRESS" "updateUIMultiplier(uint256,uint256)" 2000000000000000000 "$EFFECTIVE_AT" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "newUIMultiplier()(uint256)" --rpc-url "$RPC_URL" ``` On success, the asset emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp)`. That event fires when the update is recorded, not when the multiplier becomes active. ### Read the Live Pending State ```solidity Read the pending state theme={null} asset.uiMultiplier(); // still the current multiplier asset.newUIMultiplier(); // scheduled target asset.effectiveAt(); // flip timestamp ``` A second `updateUIMultiplier` while a pending update is live reverts `UIMultiplierUpdateExists`. To replace a pending update, cancel first, then reschedule. ### Confirm After the Effective Time When `block.timestamp >= effectiveAt`, `uiMultiplier()` returns the new multiplier. Maturation emits no event. Do not wait for a second event at the flip.`uiMultiplier()` returns `2e18` at or after `effectiveAt`. Raw `balanceOf` stays unchanged. `balanceOfUI` doubles. ## Cancel a Pending Update Call before `effectiveAt` to discard the pending split: ```solidity Cancel the pending update theme={null} asset.cancelUIMultiplierUpdate(); ``` Emits `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)`. Calling with no live pending update, including after maturity, reverts `UIMultiplierUpdateDoesNotExist`. To cancel and reschedule atomically, wrap both calls in `announce`: ```solidity Cancel and reschedule in one announcement wrap theme={null} bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(IB20Asset.cancelUIMultiplierUpdate, ()); calls[1] = abi.encodeCall(IB20Asset.updateUIMultiplier, (secondMultiplier, secondEffectiveAt)); asset.announce(calls, "2026-multiplier-02", "Rescheduled multiplier update", "https://example.com/announcements/2026-multiplier-02"); ``` ## Emergency Override Use `updateMultiplier(newMultiplier)` only when a pending update is wrong and you cannot wait for `effectiveAt`. It applies the value immediately and clears any pending update. The interface guarantees that it emits both `MultiplierUpdated` and `UIMultiplierUpdated`. The base-std stock-split guide documents the sequence as: | Situation | Events (in order) | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Live pending (`effectiveAt > block.timestamp`) | `UIMultiplierUpdateCancelled`, then `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | | Matured or no pending | `MultiplierUpdated(new)`, then `UIMultiplierUpdated(old, new, effectiveAtTimestamp)` | `MultiplierUpdated` is deprecated. Process only `UIMultiplierUpdated` to avoid handling the same update twice. ## Integrator Rules * Listen for `UIMultiplierUpdated`, not the deprecated `MultiplierUpdated`. * If `effectiveAtTimestamp > block.timestamp`, treat the update as pending until that time. * Maturation emits nothing. Do not wait for a second event at the flip. * On `UIMultiplierUpdateCancelled`, discard the pending update. * Detect a live pending update with `effectiveAt() > block.timestamp`. Do not check `effectiveAt() == 0`. ## Common Errors | Error | Cause | Fix | | --------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------- | | `AccessControlUnauthorizedAccount(caller, OPERATOR_ROLE)` | Caller lacks `OPERATOR_ROLE`. | Grant `OPERATOR_ROLE` to the operator. | | `InvalidMultiplier()` | `newMultiplier` is zero or above `MAX_UI_MULTIPLIER`. | Pass a value in `(0, MAX_UI_MULTIPLIER]`. | | `EffectiveAtInPast(effectiveAt)` | `effectiveAt <= block.timestamp`. | Pass a strictly future timestamp. | | `EffectiveAtTooFar(effectiveAt)` | `effectiveAt > type(uint64).max`. | Pass a timestamp that fits in `uint64`. | | `UIMultiplierUpdateExists(effectiveAt)` | A live pending update already exists. | Cancel first, or cancel and reschedule in one `announce`. | | `UIMultiplierUpdateDoesNotExist()` | `cancelUIMultiplierUpdate` with no live pending update, including after maturity. | Call only while `effectiveAt() > block.timestamp`. | | `AnnouncementIdAlreadyUsed(id)` | `announce` reused an `id`. | Choose a new single-use `id`. | | `InternalCallFailed(call)` | An inner call in `announce` reverted. | Fix the encoded calldata and retry. | ## See Also# Cancel Blocked Units Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/cancel-blocked-units Cancel units from a holder after removing the holder from a B20 sender allowlist. Cancel shares from a holder who is already denied by the token's transfer-sender policy. Removing the holder from an allowlist freezes outgoing transfers; `burnBlocked` then burns the specified shares and reduces total supply. Record an onchain distribution announcement. Halt transfers in an emergency. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Cancel and Verify Blocked Shares```typescript TypeScript lines wrap expandable highlight={8} theme={null} import { parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function cancelBlockedShares(token: Address, holder: Address) { const amount = parseUnits("100", 6); await sendContract({ address: token, abi: b20Abi, functionName: "burnBlocked", args: [holder, amount] }); return publicClient.readContract({ address: token, abi: b20Abi, functionName: "balanceOf", args: [holder] }); } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function cancelBlockedShares(address token, address holder) public { IB20(token).burnBlocked(holder, 100e6); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$TOKEN_ADDRESS" "burnBlocked(address,uint256)" "$BLOCKED_HOLDER" 100000000 \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "balanceOf(address)(uint256)" "$BLOCKED_HOLDER" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies.The holder balance and total supply each fall by 100 EXM. `burnBlocked` is deprecated. It destroys shares and reduces `totalSupply`; it does not move them to another account. To move a holder's balance to a safekeeping account without changing `totalSupply`, use `seizeWithMemo`, then `burn` if you still need to destroy supply. `burnBlocked` needs `BURN_BLOCKED_ROLE`, is blocked while `BURN` is paused, and reverts `AccountNotBlocked` unless the holder is denied under `TRANSFER_SENDER_POLICY`. ## See Also# Create an Asset Token Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/create-an-asset-token Create a six-decimal B20 Asset token with issuer roles, a supply ceiling, and issuer-defined metadata. This example configures a stock token. Use the [B20 Asset variant](/specifications/b20/specification-overview#asset) to represent a class of tokenized shares with configurable precision, issuer roles, supply controls, and metadata. This example creates Example Corp Class A (`EXM`) with six decimals. Gate who can hold with policies. Mint asset units to holders. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Create and Verify the Stock Token Choose the **Asset** variant when you need configurable decimals, announcements, a scheduled UI multiplier, extra metadata, or `batchMint`. Asset decimals must be in the range `[6, 18]`; values outside that range revert `InvalidDecimals`. The type is sealed into the token address at creation and cannot change.```typescript TypeScript lines wrap expandable highlight={15} theme={null} import { encodeAbiParameters, encodeFunctionData, keccak256, parseAbiParameters, parseEventLogs, stringToBytes } from "viem"; import { account } from "../../shared/clients.js"; import { B20_FACTORY, b20Abi, factoryAbi, role } from "../abi.js"; import { sendContract } from "../write.js"; export async function createStockToken() { const salt = keccak256(stringToBytes("example-class-a-v1")); const params = encodeAbiParameters( parseAbiParameters("(uint8 version,string name,string symbol,address initialAdmin,uint8 decimals)"), [{ version: 1, name: "Example Corp Class A", symbol: "EXM", initialAdmin: account.address, decimals: 6 }], ); const initCalls = ["MINT_ROLE", "BURN_BLOCKED_ROLE", "PAUSE_ROLE", "UNPAUSE_ROLE", "OPERATOR_ROLE"].map( (name) => encodeFunctionData({ abi: b20Abi, functionName: "grantRole", args: [role(name), account.address] }), ); const receipt = await sendContract({ address: B20_FACTORY, abi: factoryAbi, functionName: "createB20", args: [0, salt, params, initCalls] }); const [created] = parseEventLogs({ abi: factoryAbi, logs: receipt.logs, eventName: "B20Created" }); return created.args.token; } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function createStock(address admin) public returns (address token) { B20FactoryLib.B20AssetRoleHolders memory holders = B20FactoryLib.B20AssetRoleHolders({ minter: admin, burner: admin, burnBlocker: admin, pauser: admin, unpauser: admin, metadataAdmin: admin, operator: admin }); bytes[] memory settings = new bytes[](1); settings[0] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e6); token = StdPrecompiles.B20_FACTORY.createB20( IB20Factory.B20Variant.ASSET, keccak256("example-class-a-v1"), B20FactoryLib.encodeAssetCreateParams("Example Corp Class A", "EXM", admin, 6), B20FactoryLib.concat(B20FactoryLib.buildRoleGrants(holders), settings) ); } ``` The factory assigns a deterministic address from `(variant, sender, salt)`. Address byte `[10]` is `0x00` for Asset tokens. Reusing the same `(variant, sender, salt)` triple reverts `TokenAlreadyExists`. Optional `initCalls`, such as the role grants above, run on the new token in the same transaction. The factory drops access after `createB20` returns. See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies.The factory emits `B20Created` for the Asset variant and returns the deterministic token address. The supply cap is a technical ceiling, not a representation of authorized or outstanding shares. ## See Also# Issue Units to Holders Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/issue-units Distribute B20 Asset units to multiple approved holders in one batch. Distribute shares to multiple holders in one all-or-nothing transaction with B20 Asset's `batchMint`. Every recipient must pass the token's mint-receiver policy, and the combined issuance must stay below its supply cap. Mint asset units to holders. Gate who can hold with policies. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Issue and Verify the Shares```typescript TypeScript lines wrap expandable highlight={8} theme={null} import { parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { assetAbi, b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function issueShares(token: Address, holders: [Address, Address]) { const amounts = [parseUnits("600", 6), parseUnits("400", 6)] as const; await sendContract({ address: token, abi: assetAbi, functionName: "batchMint", args: [holders, amounts] }); const balances = await Promise.all(holders.map((holder) => publicClient.readContract({ address: token, abi: b20Abi, functionName: "balanceOf", args: [holder] }))); if (balances[0] !== amounts[0] || balances[1] !== amounts[1]) throw new Error("Unexpected issuance balances"); } ``` ```solidity Solidity lines wrap expandable highlight={8} theme={null} function issueShares(address token, address alice, address bob) public { address[] memory recipients = new address[](2); recipients[0] = alice; recipients[1] = bob; uint256[] memory amounts = new uint256[](2); amounts[0] = 600e6; amounts[1] = 400e6; IB20Asset(token).batchMint(recipients, amounts); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$TOKEN_ADDRESS" "batchMint(address[],uint256[])" \ "[$ALICE,$BOB]" "[600000000,400000000]" --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "balanceOf(address)(uint256)" "$ALICE" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies.Alice holds 600 EXM and Bob holds 400 EXM after the all-or-nothing batch. The entire batch reverts if lengths differ, a recipient fails policy, or total supply would exceed the cap. ## See Also# Pause Transfers Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/pause-transfers Pause transfers on a B20 Asset token during an incident while leaving minting and burning available. Pause the `TRANSFER` feature during an incident without stopping issuance or cancellation. B20 grants pause and unpause through separate roles so you can separate emergency response from recovery authority. Gate who can hold with policies. Record an onchain distribution announcement. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Pause and Resume Stock Transfers```typescript TypeScript lines wrap expandable highlight={7} theme={null} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function setStockTransfersPaused(token: Address, paused: boolean) { await sendContract({ address: token, abi: b20Abi, functionName: paused ? "pause" : "unpause", args: [[0]] }); const current = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "isPaused", args: [0] }); if (current !== paused) throw new Error("Unexpected transfer pause state"); } ``` ```bash CLI lines wrap expandable highlight={1,4} theme={null} base-cast send "$TOKEN_ADDRESS" "pause(uint8[])" "[0]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "isPaused(uint8)(bool)" 0 --rpc-url "$RPC_URL" base-cast send "$TOKEN_ADDRESS" "unpause(uint8[])" "[0]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" ``` `pause` and `unpause` each accept a `PausableFeature[]` array. Pass `[0]` to target `TRANSFER` only. The four pausable features are `TRANSFER` (0), `MINT` (1), `BURN` (2), and `SEIZE` (3). Pausing one feature does not affect the others. A holder can still transfer while `MINT` is paused, and a minter can still mint while `TRANSFER` is paused. Passing an empty array reverts `EmptyFeatureSet`. A feature already in the requested state is a no-op; the call does not revert. After the call, read `isPaused(feature)` to confirm the current state. The event emitted (`Paused` or `Unpaused`) carries the exact array you passed, not the resulting paused set. If a caller attempts an operation while its feature is paused, the call reverts `ContractPaused(feature)`.Transfer pause state changes without pausing mint or burn. `pause` requires `PAUSE_ROLE`. `unpause` requires `UNPAUSE_ROLE`. These are separate roles, so grant recovery authority more narrowly than emergency pause authority. ## See Also# Restrict Eligible Holders Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-rwa/restrict-eligible-holders Keep units within an approved set of holders by binding a B20 allowlist to issuance and transfers. Create an allowlist in the [Policy Registry](/specifications/b20/specification-overview#policy-registry), then apply it when shares are issued, sent, or received. Accounts remain ineligible until the policy admin adds them. Gate who can hold with policies. Burn units from a blocked account. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/build-on-base/issue-rwa/create-an-asset-token) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Create and Bind the Eligibility Policy```typescript TypeScript lines wrap expandable highlight={7,10} theme={null} import { parseEventLogs, type Address } from "viem"; import { account } from "../../shared/clients.js"; import { POLICY_REGISTRY, b20Abi, policyRegistryAbi, scope } from "../abi.js"; import { sendContract } from "../write.js"; export async function restrictStockHolders(token: Address, holders: Address[]) { const receipt = await sendContract({ address: POLICY_REGISTRY, abi: policyRegistryAbi, functionName: "createPolicyWithAccounts", args: [account.address, 1, holders] }); const [created] = parseEventLogs({ abi: policyRegistryAbi, logs: receipt.logs, eventName: "PolicyCreated" }); for (const policyScope of [scope("MINT_RECEIVER_POLICY"), scope("TRANSFER_SENDER_POLICY"), scope("TRANSFER_RECEIVER_POLICY")]) { await sendContract({ address: token, abi: b20Abi, functionName: "updatePolicy", args: [policyScope, created.args.policyId] }); } return created.args.policyId; } ``` ```solidity Solidity lines wrap expandable highlight={2,5,6,7} theme={null} function restrictStock(address token, address admin, address[] memory holders) public returns (uint64 id) { id = StdPrecompiles.POLICY_REGISTRY.createPolicyWithAccounts( admin, IPolicyRegistry.PolicyType.ALLOWLIST, holders ); IB20(token).updatePolicy(B20Constants.MINT_RECEIVER_POLICY, id); IB20(token).updatePolicy(B20Constants.TRANSFER_SENDER_POLICY, id); IB20(token).updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, id); } ``` ```bash CLI lines wrap expandable highlight={2,7} theme={null} CREATE_TX=$(base-cast send "$POLICY_REGISTRY" \ "createPolicyWithAccounts(address,uint8,address[])" "$ADMIN" 1 "[$ALICE,$BOB]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" --json | jq -r .transactionHash) POLICY_TOPIC=$(base-cast receipt "$CREATE_TX" --rpc-url "$RPC_URL" --json | jq -r '.logs[0].topics[1]') POLICY_ID=$(base-cast to-dec "$POLICY_TOPIC") for SCOPE in MINT_RECEIVER_POLICY TRANSFER_SENDER_POLICY TRANSFER_RECEIVER_POLICY; do base-cast send "$TOKEN_ADDRESS" "updatePolicy(bytes32,uint64)" "$(base-cast keccak "$SCOPE")" "$POLICY_ID" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" done ``` See the [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token) for the complete interface, roles, and policies.The same allowlist policy gates mint recipients, transfer senders, and transfer recipients. Seed all intended holders before binding an allowlist. An unlisted account becomes ineligible immediately. ## See Also# Block an Account Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/block-an-account Stop a specific address from moving your stablecoin when a compliance hold requires it, without affecting other holders. Sometimes you need to stop one address from moving your stablecoin: a compliance hold or a support request. A **blocklist** denies only the addresses you list and leaves every other holder untouched. Bind it to the sender scope so a blocked account can't send. ## Demo Burn units from a blocked account. Halt transfers in an emergency. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Block and Verify an Account```typescript TypeScript lines wrap expandable highlight={7,10} theme={null} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { POLICY_REGISTRY, policyRegistryAbi } from "../abi.js"; import { sendContract } from "../write.js"; export async function setBlocked(policyId: bigint, holder: Address, blocked: boolean) { await sendContract({ address: POLICY_REGISTRY, abi: policyRegistryAbi, functionName: "updateBlocklist", args: [policyId, blocked, [holder]], }); const authorized = await publicClient.readContract({ address: POLICY_REGISTRY, abi: policyRegistryAbi, functionName: "isAuthorized", args: [policyId, holder], }); if (authorized === blocked) throw new Error("Unexpected blocklist state"); } ``` ```solidity Solidity lines wrap expandable highlight={4} theme={null} function setBlocked(uint64 policyId, address holder, bool blocked) public { address[] memory accounts = new address[](1); accounts[0] = holder; StdPrecompiles.POLICY_REGISTRY.updateBlocklist(policyId, blocked, accounts); require(StdPrecompiles.POLICY_REGISTRY.isAuthorized(policyId, holder) != blocked, "wrong policy state"); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$POLICY_REGISTRY" "updateBlocklist(uint64,bool,address[])" \ "$BLOCKLIST_ID" true "[$ACCOUNT]" --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$POLICY_REGISTRY" "isAuthorized(uint64,address)(bool)" \ "$BLOCKLIST_ID" "$ACCOUNT" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.`isAuthorized(policyId, account)` returns `false` while the account is blocked. This only stops outgoing transfers when the blocklist is bound to `TRANSFER_SENDER_POLICY`. Unblock with the same call and `false`. ## See Also# Burn Supply Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/burn-supply Retire stablecoin supply on Base when a holder redeems for fiat, keeping circulating supply matched to reserves. When a holder redeems for fiat, they return the tokens and you burn them, keeping circulating supply matched to reserves. Burning is gated by `BURN_ROLE` and burns from the caller's own balance. ## Demo Reissue a blocked balance. Freeze all token activity. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Burn and Verify Supply```typescript TypeScript lines wrap expandable highlight={9} theme={null} import { parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function burnAndVerify(token: Address) { const before = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "totalSupply" }); const amount = parseUnits("400", 6); await sendContract({ address: token, abi: b20Abi, functionName: "burn", args: [amount] }); const after = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "totalSupply" }); if (before - after !== amount) throw new Error("Unexpected supply change"); } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function burnStablecoin(address token) public { uint256 supplyBefore = IB20(token).totalSupply(); IB20(token).burn(400e6); require(supplyBefore - IB20(token).totalSupply() == 400e6, "wrong supply change"); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$TOKEN_ADDRESS" "burn(uint256)" 400000000 \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "totalSupply()(uint256)" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.`totalSupply()` decreases by exactly 400 MUSD. `burn` removes tokens from the caller. Use an allowance and your redemption workflow to collect tokens into the burner account first. ## See Also# Issue Your Stablecoin Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/issue-your-stablecoin Create a fiat-backed stablecoin on Base with one B20 factory call. Create a fiat-backed token with one call to the [B20 Factory](/specifications/b20/specification-overview#factory), using the `STABLECOIN` variant. Decimals are fixed at `6`, and the token carries an immutable, self-declared currency code such as `USD`. The type is sealed into the token address at creation and cannot change. ## Demo Increase circulating supply. The burn operation in the B20 standard. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How the Stablecoin Variant Works Passing `STABLECOIN` to `createB20` writes discriminant byte `0x01` into position `[10]` of the token address. After the call returns: * `decimals()` returns `6` (hardcoded; you do not pass a decimals value). * `currency()` returns the immutable code you provided (for example `"USD"`). B20 checks the format only, not that the code is a real currency. * The `IB20Stablecoin` surface is live at that address. * Asset-only selectors (`announce`, `updateUIMultiplier`, `batchMint`) do not execute on this address. The `currency` field is required and must be uppercase ASCII `A` to `Z` only. An empty code reverts `MissingRequiredField`; any other byte reverts `InvalidCurrency`. ## Create and Verify the Stablecoin```typescript TypeScript lines wrap expandable highlight={26} theme={null} import { encodeAbiParameters, encodeFunctionData, keccak256, parseAbiParameters, parseEventLogs, stringToBytes } from "viem"; import { account } from "../../shared/clients.js"; import { B20_FACTORY, b20Abi, factoryAbi, role } from "../abi.js"; import { sendContract } from "../write.js"; export async function createStablecoin() { const salt = keccak256(stringToBytes("merchant-usd-v1")); const params = encodeAbiParameters( parseAbiParameters( "(uint8 version,string name,string symbol,address initialAdmin,string currency)", ), [{ version: 1, name: "Merchant USD", symbol: "MUSD", initialAdmin: account.address, currency: "USD" }], ); const initCalls = [ encodeFunctionData({ abi: b20Abi, functionName: "grantRole", args: [role("MINT_ROLE"), account.address], }), encodeFunctionData({ abi: b20Abi, functionName: "updateSupplyCap", args: [10_000_000n * 10n ** 6n], }), ]; const receipt = await sendContract({ address: B20_FACTORY, abi: factoryAbi, functionName: "createB20", args: [1, salt, params, initCalls], }); const [created] = parseEventLogs({ abi: factoryAbi, logs: receipt.logs, eventName: "B20Created" }); return created.args.token; } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function createStablecoin(address admin) public returns (address token) { B20FactoryLib.B20RoleHolders memory holders = B20FactoryLib.B20RoleHolders({ minter: admin, burner: admin, burnBlocker: admin, pauser: admin, unpauser: admin, metadataAdmin: admin }); bytes[] memory roles = B20FactoryLib.buildRoleGrants(holders); bytes[] memory settings = new bytes[](1); settings[0] = B20FactoryLib.encodeUpdateSupplyCap(10_000_000e6); token = StdPrecompiles.B20_FACTORY.createB20( IB20Factory.B20Variant.STABLECOIN, keccak256("merchant-usd-v1"), B20FactoryLib.encodeStablecoinCreateParams("Merchant USD", "MUSD", admin, "USD"), B20FactoryLib.concat(roles, settings) ); } ``` The `params` blob is ABI-encoded with a leading `version` byte (currently `1`) as `B20StablecoinCreateParams`: `version`, `name`, `symbol`, `initialAdmin`, and `currency`. Optional `initCalls` run on the new token in the same transaction; the factory drops access after they complete. See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.The factory emits `B20Created`, and the returned token address is ready for minting. B20 tokens are open by default. Configure roles, policies, and the supply cap during creation before issuing value. ## See Also# Mint Supply Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/mint-supply Issue new stablecoin supply on Base as reserves grow, gated by a minter role and an optional supply cap. When fiat lands in reserves, mint matching supply. Minting is gated by `MINT_ROLE`, and an optional supply cap keeps circulation from exceeding your reserves. ## Demo Increase circulating supply. Allowlist eligible holders. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Mint and Verify Supply```typescript TypeScript lines wrap expandable highlight={8} theme={null} import { parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function mintAndVerify(token: Address, holder: Address) { const amount = parseUnits("1000", 6); await sendContract({ address: token, abi: b20Abi, functionName: "mint", args: [holder, amount] }); const balance = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "balanceOf", args: [holder] }); if (balance < amount) throw new Error("Minted balance was not recorded"); return balance; } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function mintStablecoin(address token, address holder) public { IB20(token).mint(holder, 1_000e6); require(IB20(token).balanceOf(holder) >= 1_000e6, "mint not recorded"); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$TOKEN_ADDRESS" "mint(address,uint256)" "$HOLDER" 1000000000 \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "balanceOf(address)(uint256)" "$HOLDER" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.The holder balance increases by `1,000,000,000` base units, or 1,000 MUSD. The caller needs `MINT_ROLE`, the recipient must pass `MINT_RECEIVER_POLICY`, and the mint must remain under the supply cap. ## See Also# Pause Activity Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/pause-activity Halt transfers, mints, or burns on your stablecoin independently during an incident, then resume when it's resolved. If something goes wrong, halt activity fast. B20 pausing is **granular**: stop transfers, mints, or burns independently. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate, so the ability to stop the token can be held more widely than the ability to resume it. The four pausable features are `TRANSFER`, `MINT`, `BURN`, and `SEIZE` (enum values `0` to `3`). Each is independent: pausing `MINT` and `BURN` leaves `TRANSFER` active. A caller who holds `MINT_ROLE` still cannot mint while `MINT` is paused. ## Demo Remove tokens from supply. Supply cap in the B20 standard. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Pause and Resume Transfers```typescript TypeScript lines wrap expandable highlight={7} theme={null} import { type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function setTransfersPaused(token: Address, paused: boolean) { await sendContract({ address: token, abi: b20Abi, functionName: paused ? "pause" : "unpause", args: [[0]], }); const current = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "isPaused", args: [0] }); if (current !== paused) throw new Error("Pause state did not change"); } ``` ```bash CLI lines wrap expandable highlight={1,4} theme={null} base-cast send "$TOKEN_ADDRESS" "pause(uint8[])" "[0]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "isPaused(uint8)(bool)" 0 --rpc-url "$RPC_URL" base-cast send "$TOKEN_ADDRESS" "unpause(uint8[])" "[0]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.`isPaused(0)` returns `true` after pausing and `false` after resuming. `pause` and `unpause` accept a `PausableFeature[]` array. Passing an empty array reverts `EmptyFeatureSet`. Transfer is enum value `0`; never pass a hash such as `keccak256("TRANSFER")`. A feature already in the requested state is a no-op. The call does not revert. ## Pausable Features | Feature | Enum value | Gates | | ---------- | ---------- | --------------------------------------------- | | `TRANSFER` | `0` | `transfer`, `transferFrom`, and memo variants | | `MINT` | `1` | `mint` and `mintWithMemo` | | `BURN` | `2` | `burn`, `burnWithMemo`, `burnBlocked` | | `SEIZE` | `3` | `seizeWithMemo` | When a paused feature blocks a call, the transaction reverts `ContractPaused(feature)`. The error names only the one feature that blocked the call. ## See Also# Reconcile with Memos Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/reconcile-with-memos Tag stablecoin operations with an onchain reference so you can match them to offchain records at scale. Tie onchain activity back to your books. A **memo** attaches a `bytes32` reference to an operation and emits a `Memo` event alongside it, so reconciliation is a log query instead of a deposit address per customer. Every core call has a memo variant: `mintWithMemo`, `burnWithMemo`, `transferWithMemo`, and `transferFromWithMemo`. If you operate the merchant side and need a settlement report across captures, refunds, and payouts, see [Reconcile payments](/build-on-base/accept-payments/reconcile-payments). ## Demo Deny a specific address. Pause controls in the B20 standard. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Attach and Read an Invoice Memo```typescript TypeScript lines wrap expandable highlight={7,10} theme={null} import { hexToString, parseEventLogs, parseUnits, stringToHex, type Address } from "viem"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function payWithMemo(token: Address, merchant: Address) { const memo = stringToHex("invoice-8842", { size: 32 }); const receipt = await sendContract({ address: token, abi: b20Abi, functionName: "transferWithMemo", args: [merchant, parseUnits("25", 6), memo], }); const [event] = parseEventLogs({ abi: b20Abi, logs: receipt.logs, eventName: "Memo" }); return hexToString(event.args.memo, { size: 32 }).replace(/\0+$/, ""); } ``` ```bash CLI lines wrap expandable highlight={2} theme={null} MEMO=$(base-cast format-bytes32-string "invoice-8842") TX=$(base-cast send "$TOKEN_ADDRESS" "transferWithMemo(address,uint256,bytes32)" \ "$MERCHANT" 25000000 "$MEMO" --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" --json | jq -r .transactionHash) base-cast receipt "$TX" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.The receipt contains `Transfer` followed immediately by `Memo`, with `invoice-8842` encoded as `bytes32`. Join a memo to the preceding operation with `(transactionHash, logIndex - 1)`. Keep the offchain invoice ID unique. ## See Also# Recover Funds Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/recover-funds Reclaim and reissue stablecoin from a blocked account on Base, for lost keys or a legal hold. Occasionally you need to move a balance out of a blocked account: a holder who lost their keys, or a balance you must reissue. Reclaim it with `burnBlocked`, then mint the same amount to the correct address. It's gated by its own `BURN_BLOCKED_ROLE` and only works on an account that is already blocked. ## Demo The app-side B20 memo payment flow. Reconcile at scale with the CDP SQL API. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## Burn and Reissue the Blocked Balance```typescript TypeScript lines wrap expandable highlight={8,9} theme={null} import { parseUnits, type Address } from "viem"; import { publicClient } from "../../shared/clients.js"; import { b20Abi } from "../abi.js"; import { sendContract } from "../write.js"; export async function recoverBlockedFunds(token: Address, blocked: Address, replacement: Address) { const amount = parseUnits("50", 6); await sendContract({ address: token, abi: b20Abi, functionName: "burnBlocked", args: [blocked, amount] }); await sendContract({ address: token, abi: b20Abi, functionName: "mint", args: [replacement, amount] }); const balance = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "balanceOf", args: [replacement] }); if (balance < amount) throw new Error("Replacement balance was not issued"); } ``` ```solidity Solidity lines wrap expandable highlight={1} theme={null} function recoverStablecoin(address token, address blocked, address replacement) public { IB20(token).burnBlocked(blocked, 50e6); IB20(token).mint(replacement, 50e6); require(IB20(token).balanceOf(replacement) >= 50e6, "replacement not funded"); } ``` ```bash CLI lines wrap expandable highlight={1} theme={null} base-cast send "$TOKEN_ADDRESS" "burnBlocked(address,uint256)" "$BLOCKED" 50000000 \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast send "$TOKEN_ADDRESS" "mint(address,uint256)" "$REPLACEMENT" 50000000 \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" base-cast call "$TOKEN_ADDRESS" "balanceOf(address)(uint256)" "$REPLACEMENT" --rpc-url "$RPC_URL" ``` See the [B20 token standard](/specifications/b20/specification-overview) for the complete interface, roles, and policies.The blocked balance falls and the replacement address receives the same amount, leaving circulating supply unchanged. `burnBlocked` requires `BURN_BLOCKED_ROLE` and only succeeds while the source is denied by `TRANSFER_SENDER_POLICY`. ## See Also# Restrict Who Can Hold It Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/issue-stablecoins/restrict-who-can-hold Limit transfers of your stablecoin to accounts your KYC program has approved, using B20 transfer policies. Keep your stablecoin within a known set of holders with an **allowlist**: transfers only settle between accounts your KYC program has approved. You manage the list in the [Policy Registry](/specifications/b20/specification-overview#policy-registry) and bind it to the token's transfer scopes. ## Demo Deny a specific address. Match payments using onchain memos. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. New to B20? See the [B20 Token Standard](/specifications/b20/specification-overview) for the concepts and a full launch walkthrough. These samples target `base-std@be6d045`, `viem@2.55.11`, and Base Foundry `v1.1.1`. ## How Policies Work The Policy Registry is a singleton precompile that stores each member list once. A token stores only a `uint64` policy ID per scope. When a gated function runs, the token calls `isAuthorized(policyId, account)` on the registry. Many tokens can share one policy; an update to the policy is immediately visible to every token that references it. Scopes gate specific functions. `TRANSFER_SENDER_POLICY` and `TRANSFER_RECEIVER_POLICY` check the sender and recipient on every `transfer` and `transferFrom`. `MINT_RECEIVER_POLICY` checks the recipient on every `mint`. All three default to `ALWAYS_ALLOW` (`0`) until you bind a policy. An **allowlist** authorizes only accounts in the set. An empty allowlist authorizes nobody, so seed your intended holders before binding the policy. ## Create and Bind a Holder Allowlist```typescript TypeScript lines wrap expandable highlight={7,10,16} theme={null} import { parseEventLogs, type Address } from "viem"; import { account, publicClient } from "../../shared/clients.js"; import { POLICY_REGISTRY, b20Abi, policyRegistryAbi, scope } from "../abi.js"; import { sendContract } from "../write.js"; export async function createHolderAllowlist(token: Address, holders: Address[]) { const receipt = await sendContract({ address: POLICY_REGISTRY, abi: policyRegistryAbi, functionName: "createPolicyWithAccounts", args: [account.address, 1, holders], }); const [created] = parseEventLogs({ abi: policyRegistryAbi, logs: receipt.logs, eventName: "PolicyCreated" }); const policyId = created.args.policyId; for (const policyScope of [scope("TRANSFER_SENDER_POLICY"), scope("TRANSFER_RECEIVER_POLICY")]) { await sendContract({ address: token, abi: b20Abi, functionName: "updatePolicy", args: [policyScope, policyId] }); } const saved = await publicClient.readContract({ address: token, abi: b20Abi, functionName: "policyId", args: [scope("TRANSFER_RECEIVER_POLICY")] }); if (saved !== policyId) throw new Error("Policy was not bound"); return policyId; } ``` ```solidity Solidity lines wrap expandable highlight={2,5,6} theme={null} function restrictStablecoin(address token, address admin, address[] memory holders) public returns (uint64 id) { id = StdPrecompiles.POLICY_REGISTRY.createPolicyWithAccounts( admin, IPolicyRegistry.PolicyType.ALLOWLIST, holders ); IB20(token).updatePolicy(B20Constants.TRANSFER_SENDER_POLICY, id); IB20(token).updatePolicy(B20Constants.TRANSFER_RECEIVER_POLICY, id); require(IB20(token).policyId(B20Constants.TRANSFER_RECEIVER_POLICY) == id, "policy not bound"); } ``` ```bash CLI lines wrap expandable highlight={2,8} theme={null} CREATE_TX=$(base-cast send "$POLICY_REGISTRY" \ "createPolicyWithAccounts(address,uint8,address[])" "$ADMIN" 1 "[$ALICE,$BOB]" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" --json | jq -r .transactionHash) POLICY_TOPIC=$(base-cast receipt "$CREATE_TX" --rpc-url "$RPC_URL" --json | \ jq -r '.logs[] | select(.address | ascii_downcase == "0x8453000000000000000000000000000000000002") | .topics[1]' | head -1) POLICY_ID=$(base-cast to-dec "$POLICY_TOPIC") for SCOPE in TRANSFER_SENDER_POLICY TRANSFER_RECEIVER_POLICY; do base-cast send "$TOKEN_ADDRESS" "updatePolicy(bytes32,uint64)" "$(base-cast keccak "$SCOPE")" "$POLICY_ID" \ --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" done ``` The token returns the new policy ID for both transfer policy scopes. An allowlist denies every account not in the policy. Seed intended holders before binding it, and retain the `PolicyCreated` ID. Membership batches are capped at 64 accounts per call; call `updateAllowlist` to add more after creation. ## Update Membership After creation, only the policy admin can add or remove accounts. Call `updateAllowlist(policyId, true, accounts)` to add and `updateAllowlist(policyId, false, accounts)` to remove. The change is visible to every token that references the policy on the next call. No second `updatePolicy` is needed on the token. To combine a KYC allowlist with a sanctions blocklist, create an `INTERSECT` composite policy referencing both simple policies, then bind the composite ID to the token's scopes. See the [B20 token standard](/specifications/b20/specification-overview) for the full policy type reference. ## See Also# Overview Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/overview Build financial products on Base by outcome: integrate DeFi, tokenize assets, issue stablecoins, or accept payments. Build with native Base standards or integrate protocols already deployed on the network. Pick the financial outcome you need, then follow a short guide for the specific action your product performs. ## Solutions Deny a specific address. Policy hooks in the B20 standard. ## Build the Foundation Add trading, direct lending, collateralized borrowing, or a vault-based earn product. Represent real-world assets with B20 Asset issuance, holder controls, and distributions. Launch a fiat-backed token with minting, compliance, and reconciliation built in. Build the full payment lifecycle: request, authorize, capture, verify, refund, reconcile, and pay out. Try everything on a disposable, fully-featured test network. # Test on Vibenet Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/build-on-base/test-on-vibenet Build and test against Base's newest chain-level features on Vibenet, Base's experimental preview network, and track what's live at chain.base.org/vibenet. Vibenet is Base's experimental preview network, a devnet where new chain-level features go live before they roll out to Sepolia or Mainnet. Use it to build against cutting-edge Base capabilities. Network details, node operations, and protocol specs. JSON-RPC, Flashblocks, and SDK reference. Vibenet is for experimentation only. It is not intended for production or user-facing applications, and its state may be reset without notice. ## See What's Live on Vibenet [chain.base.org/vibenet](https://chain.base.org/vibenet) is the hub for Vibenet: track the latest features as they ship, browse the block explorer, and request testnet ETH from the faucet.If the embed doesn't load, open [chain.base.org/vibenet](https://chain.base.org/vibenet) directly in a new tab. ## Network Details | | | | :------------------ | :------------------------------------------------------------------------- | | **Network name** | Base Vibenet | | **RPC endpoint** | [rpc.vibes.base.org](https://rpc.vibes.base.org) | | **Chain ID** | 84538453 | | **Currency symbol** | ETH | | **Faucet** | [chain.base.org/vibenet/faucet](https://chain.base.org/vibenet/faucet) | | **Block explorer** | [chain.base.org/vibenet/explorer](https://chain.base.org/vibenet/explorer) | To add Vibenet to your wallet, see [Connecting to Base](/base-chain/quickstart/connecting-to-base). ## Get Testnet ETH Vibenet transactions need gas. Request testnet ETH from the [Vibenet faucet](https://chain.base.org/vibenet/faucet), or drip programmatically: ```bash Terminal theme={null} curl -X POST https://api.vibes.base.org/api/vibenet/faucet/drip \ -H "content-type: application/json" \ -d '{"address":"0xYourAddress"}' ``` ## What's Available Now## Next Steps Base's native token standard: roles, supply caps, policy gating, and memos built into the chain. Test canonical 200ms blocks and migrate Flashblocks integrations. # Accept Payments Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/accept-payments Choose an onchain payment lifecycle for checkout, agentic payments, reconciliation, refunds, and payouts on Base. Accept token payments on Base with the settlement model your product needs. Start with an immediate USDC or B20 transfer, or separate buyer authorization from merchant capture when fulfillment happens later. The same lifecycle extends to x402 APIs, scheduled charges, refunds, payouts, and reconciliation. ## Demo Add Vibenet to your wallet and development environment. Deploy a token on Vibenet with one factory call. The demo above is mock only. If you want to see onchain demos on Vibenet, head to [Base chain demos](https://chain.base.org/demos). ## Payment Lifecycle | Stage | What happens | Start with | | -------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Request | The buyer transfers immediately for a simple checkout | [Request a payment](/build-on-base/accept-payments/request-a-payment) | | Authorize | The buyer signs a bounded payment without settling yet | [Authorize a payment](/build-on-base/accept-payments/authorize-a-payment) | | Capture | The merchant submits the authorization when ready | [Capture an authorization](/build-on-base/accept-payments/capture-an-authorization) | | Confirm | Your backend verifies confirmed logs and claims the order once | [Verify a payment](/build-on-base/accept-payments/verify-a-payment) | | Return or distribute | You refund the payer or pay downstream recipients | [Refund a payment](/build-on-base/accept-payments/refund-a-payment) | ## Take a Payment## Confirm and Reconcile Settle USDC immediately or attach an order memo to B20. Collect an exact EIP-3009 authorization for later capture. Settle the stored authorization from the merchant account. Authorize a maximum and charge the final total safely. Expire or cancel an authorization you will not capture. Run recurring charges within a spend permission. ## Return and Pay Out Validate settlement and claim each order once. Subscribe to transfers and backfill confirmed blocks. Export captures, refunds, and payouts into your ledger. ## Accept Agentic Payments Return funds to the verified payer and track refundable balance. Pay a bounded recipient batch under one reference. Distribute exact basis-point shares without stranded dust. # Base Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/base The blockchain for global finance. Base is built by Coinbase, trusted by leading institutions, and open to all. Stablecoin issuance, payments, and compliance controls ship as native chain primitives you can use out of the box, without building or auditing your own contracts. Transactions settle in under a second, and cost less than one cent. Protect a fixed-price route with x402 `exact`. Authorize a maximum and settle measured usage with `upto`. Advance vouchers per request and settle channels in batches. Enforce network, asset, and spend policy before an agent signs. # Base Batches Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/base-batches Apply to Base Batches, an accelerator with investment, mentorship, and a demo day for early-stage teams building on Base. Base Batches is an accelerator for early-stage teams building the future of finance on Base, run by the [Base Ecosystem Fund](/get-started/base-ecosystem-fund). Each cohort is a short, virtual program that ends with a demo day in front of investors. Learn more on the [program page](https://www.base.org/batches). ## What You Get * A \$100,000 investment from the Base Ecosystem Fund. * An 8-week virtual program with a dedicated advisor and weekly support. * Access to subject-matter experts across the Base ecosystem. * A demo day in front of a curated group of venture investors. ## Who It's For Early-stage teams, from pre-product to post-MVP, that have not raised a formal seed round and are committed to Base as their primary network. Base Batches looks for founders building in: * Trading * Payments * Agents * Financing * Asset issuance ## Apply Cohorts run twice a year. Check the [program page](https://www.base.org/batches) for current dates before you apply. Add direct lending, collateralized borrowing, or a vault-based earn product. Represent real-world assets with B20 Asset controls and distributions. A stock token is one example. Launch a fiat-backed stablecoin with minting, compliance, and reconciliation onchain. Take instant stablecoin and agent-driven payments with low fees. # Base Protocol Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/base-chain Explore Base as a chain: connect to its networks, use native primitives, understand transactions and network systems, and operate infrastructure. Base is an EVM-compatible chain with its own native primitives, transaction pipeline, network configuration, and protocol specification. Use the Base Protocol section to connect to the chain, understand how it works, and operate infrastructure against it. ## Start Using Base Submit your team for the next cohort. Pre-seed and seed investment for teams building on Base. ## Use Chain-Native Primitives Find the practical entry points for integrating an app, wallet, contract, bridge, or infrastructure service. Configure Base Mainnet, Base Sepolia, or Vibenet in your wallet, app, or development environment. Fund a Base Sepolia address with testnet ETH and supported test tokens. Move assets to and from Base through supported routes. ## Understand and Operate the Network Use Base's native token standard for cheaper transfers, built-in controls, memos, and asset variants. Understand smart accounts that send ordinary Base transactions without separate bundler infrastructure. Learn how Base calculates execution and Ethereum security fees. Understand priority fees, block ordering, and 200 ms Flashblock preconfirmations. Choose the confirmation stage that matches your application's security requirements. Find Base Mainnet and Base Sepolia system contract addresses. # Base Ecosystem Fund Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/base-ecosystem-fund The Base Ecosystem Fund backs pre-seed and seed teams building onchain businesses on Base, in partnership with Coinbase Ventures. The Base Ecosystem Fund is the strategic investment arm of Base, run in partnership with Coinbase Ventures. It backs early-stage teams building onchain businesses on Base. Learn more on the [fund page](https://www.base.org/ecosystem-fund). ## What It Offers * Pre-seed and seed investment for teams building on Base. * Hands-on ecosystem support and partnership introductions. * Partner credits from providers such as AWS, Azure, and Alchemy. * Priority access to Coinbase Prime, Coinbase Business, and onramp APIs. ## Who It's For Pre-seed and seed founders building enduring onchain primitives and businesses that drive real economic activity, across: * Trading * Payments * AI agents * Other onchain businesses ## Apply Explore Base's consensus, execution, bridging, proving, and network-system specifications. Plan around transaction gas limits, block capacity, and RPC endpoint limits. Install, configure, tune, and troubleshoot Base node infrastructure. Diagnose pending, rejected, failed, or slow transactions. # Base Services Hub Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/base-services-hub A collection of services for building on Base. The Base Services Hub is your one‑stop directory for exclusive discounts on software and services that help Base projects ship faster, scale growth and build onchain. If you would like to provide discounts to the Base ecosystem, please [apply through the service provider form](https://forms.gle/B8u1TTqb1jyVjEYG6) and a team member will be in touch. ## Service Providers Thank you to all the teams supporting the Base ecosystem and its builders! If you are a builder and don't see the service you are looking for listed below, you can make a request for it to be added by filling out this [form](https://forms.gle/VFzJWQ4JfHxoPja27). | Company Name | Category | Description | Discount | Instructions for Redemption | | :------------------------------------------------------------------------- | :-------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Coinbase Developer Platform](https://www.coinbase.com/developer-platform) | RPC infrastructure, wallets, and developer tooling | APIs and infrastructure for building on Base, including Node, Paymaster, Onramp, Wallets, and Onchain Data. | Free credits when you sign up. | Create a CDP account to claim the available signup credits. | | [0xSplits](https://splits.org/) | Onchain operations | We build apps, contracts, and developer tools that make it easy for builders to manage onchain treasuries, revenues, and expenses. | No-fee swaps, up to \$100/mo as a gas stipend, and dedicated support via Slack or Telegram | Users will need to reach out to [base@splits.org](mailto:base@splits.org) after signing up to redeem this offer. | | [Acctual](https://www.acctual.com/) | Invoicing | The easiest way to pay or send an invoice (AP/AR) in crypto and fiat. | 3 months of fee free invoicing on Base. | Acctual users who invoice on Base will receive fee free invoicing for the first 3 months, on all Base invoices. Pitch your team for pre-seed or seed investment. An accelerator with a \$100K investment and a demo day.
Users will need to reach out to [support@acctual.com](mailto:support@acctual.com) to redeem this offer. | | [Adevar Labs Inc.](https://www.adevarlabs.com/) | Security | We audit Base-native protocols end to end—from Solidity smart contracts to OP Stack infrastructure—delivering audits, formal verification, infra reviews, and custom fuzzing before mainnet. | 30% discount on all our services (Whiteglove Audits , Formal Verification, Infrastructure Audits and Fuzzing) | Mention Base Services Hub in your application via our website. | | [Aetheryc](https://base.aetheryc.com) | AI & Security Infrastructure and Audits | AI-powered matchmaking platform that connects you directly & instantly to the perfect vetted security experts and tools. | Unlimited markup-free security audits, effectively saving you 60-90% in costs compared to traditional audit firms. | 1. Visit base.aetheryc.com and sign up
2. Enter organization and contact details
3. Our team will grant you access shortly
For any questions: [george@aetheryc.com](mailto:george@aetheryc.com)
or Telegram: @GeorgeAetheryc | | [Alchemy](https://www.alchemy.com/) | RPC Infrastructure, Wallets, Developer Tooling | The complete blockchain developer platform trusted by leading fintechs and developers worldwide. | Up to \$5,000 in Alchemy Credits | Please apply at [https://www.alchemy.com/startup-program/base](https://www.alchemy.com/startup-program/base). The Alchemy team will then be in touch to learn more about what you’re building and advise on next steps. | | [Almanax](https://www.almanax.ai/) | AI & Security | Almanax is an AI Security Engineer that uses LLMs to find security vulnerabilities every time companies push new code. | Base builders can get their first three months of Almanax premium plan for free. | Sign up for Almanax's product at [https://app.almanax.ai/](https://app.almanax.ai/)
Fill out this form and enter this promo code in the message field: "BASE-ALMANAX-DEAL"
Once approved, you'll receive a confirmation emails. | | [Anchor Zero](https://anchorzero.com) | Tax Planning | AnchorZero Roth IRAs can eliminate capital gains tax on pre-launch token investments | Waive all implementation fees | Mention you are building on Base in your introductory call with AnchorZero. | | [Api3](https://www.api3.org/) | Oracle's / Data Infrastructure | API3 is an oracle service that delivers Real World Price Feeds to your smart contract. The Price feeds provided allow dapps to regain lost value with Oracle Extractable Value built in to the feed. | If you are a lending dapp deploying on BASE, stable coin, morpho curator, borrow/lending dapp we will provide oracle services to your markets. | Contact: [http://t.me/billyjitsu](http://t.me/billyjitsu)
Or Request: [https://api3dao.typeform.com/to/TBTu8bJt](https://api3dao.typeform.com/to/TBTu8bJt)
The team will reach out and discuss the options for gas grants for oracle services. | | [Artemis](https://artemis.xyz) | Onchain Analytics | Artemis standardizes digital finance data into a single open data platform. Metrics that matter for digital finance. All in one place. | Artemis is offering free, out-of-the-box onchain metrics dashboards for Base builder's applications. | Please fill out the [Google Form](https://forms.gle/ZDS9LkxSBJVJonR36) with your application metadata and contract information. We will contact your email once your application dashboard has been created. | | [Birdeye ](https://bds.birdeye.so/) | Data Analytics - Data API - Developer Tools | Birdeye Data Services is a high-performance data provider that delivers real-time, accurate, and comprehensive on-chain data across tokens, wallets, trades, and protocols. | - Startup/Projects get 30% OFF for first 6 months - Free access to our full Business Lite package (valued at \$299) for teams participating in Hackathons or Base Batches during the program period. | Apply through the [Birdeye Data Services application form](https://docs.google.com/forms/d/e/1FAIpQLSfkv8JWR1WWq7Biqx5yyDvF6belUtdbR37mddludfu-boI34Q/viewform)
We will be in touch once the application has been reviewed. For any other inquiries, please reach out to BDS on Telegram: @birdeye\_data. | | [Blockmachine](https://blockmachine.io/base-rpc) | RPC Infrastructure & Developer Tooling | Blockmachine is an enterprise-grade RPC and archive node provider for Base, with savings driven by competing independent node operators. Responses are cryptographically verified before delivery. | First month free on any plan | Please fill out our [google form](https://forms.gle/vtm1dxZFHfnXrBCdA) and we'll get back to you within a day | | [Cantina](https://cantina.xyz/welcome) | Security | Cantina is the one-stop shop for the highest quality security researchers and solutions. Reduce the likelihood of hacks, time spent, and context lost. | 10% off all services including audits, audit competitions, pen-testing, architecture reviews, fuzzing/unit/e2e testing 50% off of bug bounty hosting for the first year | [https://cantina.xyz/introduction/base-cantina](https://cantina.xyz/introduction/base-cantina) | | [Chainalysis Hexagate](https://www.chainalysis.com) | Security | Hexagate provides real-time automated alerts and responses to stop hacks, exploits, and financial risks while protecting TVL and reputation. Trusted by Coinbase, Polygon, Mantle, and many others. | Free version of Hexagate | Apply through the [Hexagate for Base form](https://hexagate.typeform.com/HexagateForBase). | | [Coinwatch](https://coinwatch.co/) | Market Making | We help projects get the best market making deals and track their market makers to ensure they deliver on their promises. | 15% discount for 1 year of Gold tier | Fill out this [typeform](https://form.typeform.com/to/h4Xq2TF9?typeform-source=coinwatch.co)
Under the "Any additional details or questions for us?" section, input the code: BASExCOINWATCH
We will get back to you and apply the discount at the time of payment. | | [Conduit](https://conduit.xyz) | Chain Infrastructure | Conduit is the leading chain infra provider, powering 55 blockchains on ethereum including Katana, Plume, Zora, and many more. | 10% off the first year for a Conduit Base L3. | Discount must be claimed via our Sales team, just mention you'd like to take part before a contract is signed with Conduit and we can apply the discount. | | [Consider It Done Technologies (CIDT)](https://consideritdone.tech) | Development Services and Smart Contracts | Full-stack product and smart contract studio helping Base teams ship MVPs fast with secure architecture, polished UX, and production-grade DevOps | Free 60-min Base Technical Discovery + written architecture plan + estimate. 10% off first phase if kickoff is within 30 days | Contact for help:
Email: [sales@consideritdone.tech](mailto:sales@consideritdone.tech) (or [oleh.savenko@consideritdone.tech](mailto:oleh.savenko@consideritdone.tech))
Telegram: @savenoleh
Calendly: [https://calendly.com/cidt-sales/introductory-meeting](https://calendly.com/cidt-sales/introductory-meeting)
We reply within 2 business days to confirm eligibility and schedule the session. After the call, you receive a 1–2 page Architecture Brief + delivery estimate. If you choose to proceed, we apply 10% off the first phase (kickoff within 30 days). | | [Crust Network](https://www.crust.network/) | Storage | Decentralized Storage Services on Base | Applicants can receive 1000 \$CRU as free storage credits | Please fill out this [form](https://forms.cloud.microsoft/r/v3zdn7bVdK?origin=lprLink) to apply. | | [Decubate Technologies](https://decubate.com/tms) | Token Management | Decubate's TMS: All-in-one, white-labeled compliant tokenization solution. Minting, vesting, staking, lockups aligned with MiCAR—no coding needed. | Minimum 20% discount on Decubate's TMS—the all-in-one, white-labeled token management system with code-free minting, vesting, staking, lockups, tier systems, and more | Please fill our [form](https://share-eu1.hsforms.com/1ZOvFJZMnQKCPQk-IwyIWoQf5bmh) and mention 'Base' 'under where did you find us?' | | [Dune](https://www.dune.com/home) | Data Analytics - Data API - Developer Tools | Dune is a web3 data platform that lets anyone query, visualize, and share blockchain data. It’s used by analysts, builders, and communities to make onchain insights accessible and actionable. | 20% on any annual plans. | Email [support@dune.com](mailto:support@dune.com) with your company/project name using your work email. | | [Dynamic](https://dynamic.xyz) | Wallet Infrastructure | Dynamic combines authentication, smart wallets, and secure key management into one flexible SDK. Get the most multi-chain coverage across chains and third-party wallets. | Base builders can get 3 months free of our \$99/month Growth plan, which supports up to 2,000 MAUs. | Fill out this [form](https://d9hc0.share.hsforms.com/2CIpNaX14T1Cv1erD_2ou1A) in detail. Once the team receives your app, we'll review and get in touch. Note: One discount available per team. | | [FailSafe](https://www.getfailsafe.com) | Infrastructure, CyberSecurity, Audits | FailSafe provides real-time blockchain risk monitoring and smart contract audit solutions for protocols, stablecoins, and digital asset platforms across global markets. | Get \$3,000 off your first smart contract audit or monitoring subscription with FailSafe. Ideal for Base stablecoin issuers, or DeFi platforms looking to strengthen on-chain security and protect funds. | Email [wui@getfailsafe.com](mailto:wui@getfailsafe.com) with github repo of codebase for a quote on a security audit. Discount will be applied once a commercial contract is signed. | | [Firepan](https://firepan.com/) | Security | AI-powered smart contract security that runs 24/7. Firepan scans every commit, detects vulnerabilities early, and prevents exploits before deployment - continuous protection without \$150k audits. | 80% off the first month to try all of the features of a deep scan. | Sign up at Firepan.com, connect your GitHub repo, and launch your first Deep Scan in minutes. Base Builders get 80% off the first month to test every feature using code "FIREPAN80" at checkout. Add the coupon code at Stripe checkout. | | [Fjord Foundry](https://www.fjordfoundry.com/) | Fundraising / Token Sale | Connecting innovative projects and community backers through on-chain capital formation, with over \$1bn raised since 2021. | Free Premium Marketing | To claim this offer, simply tell us you discovered it through the Base Builder Services Hub when you contact Fjord. If your project passes our due‑diligence review and is selected as a launch partner, you’ll be eligible. | | [FLock.io](https://www.flock.io/) | AI | FLock.io is the first decentralized AI training platform combining Federated Learning and blockchain to enable secure, privacy-preserving model training. | Base ecosystem projects get up to 50% off Qwen tokens using FLock.io-trained models or other major Qwen variants, plus: 1 free FLock.io training task and 1hr free AI consultation. | Please fill out this [Google Form](https://forms.gle/N8We623NQdAppEj26) with your application and contract information.
FLock.io team will contact you once we receive your application. | | [Flow](http://flowonbase.com) | FX, Ramping, Payments | Flow empowers projects with seamless FX, global payments, and local‐currency on/off-ramping — and offers its best pricing to those building or migrating onto Base. | For Base Builders, we offer 15% minimum discount on global ramping requirements & zero license fee for white label products. Further discounts available volume dependent. | Get market-leading FX, on/off-ramp, and global payment rates. We offer unmatched pricing that beats any verified competitor quote — book a consultation at flowonbase.com | | [Galxe](https://www.galxe.com) | Growth, distribution and infrastructure. | Galxe is web3’s leading growth platform and distribution network, trusted by 36M+ users and over 7.8K + brands globally. | Exclusive 10% discount on all Galxe Business+ plans, giving Base builders access to Galxe’s unified, battle-tested infrastructure to scale growth and distribution. | Builders can complete onboarding at [https://dashboard.galxe.com/business+](https://dashboard.galxe.com/business+) and must explicitly specify that they are coming from the Base ecosystem during the signup process. The Galxe team will review and verify that the project is building on Base before applying the discount. If assistance is needed at any stage, builders may reach out to the Galxe team directly for support. | | [GetBlock](https://getblock.io) | Infrastructure provider | GetBlock is a Web3 infrastructure provider that offers a suite of APIs and tools to help developers build and scale decentralized applications (dApps) on top of 75+ blockchain protocols. | 50% off the first month on all shared node plans | To redeem this offer, reach out via [https://getblock.io/contact](https://getblock.io/contact) with your UID and the promo code Welcome Treat, mentioning that you are building on Base. | | [Glass Markets](https://glassmarkets.io/) | Data | Empowering token foundations with data and strategic advice to optimize liquidity across exchanges and hold market makers accountable. | 25% of annual data subscription packages | Reach out to [base@glassmarkets.io](mailto:base@glassmarkets.io) with a brief description of what you’re building with the BASE ecosystem to the redeem offer. | | [GrailPay](https://www.grailpay.com) | Authentication, Fraud, Account Validation | GrailPay authenticates bank accounts and stablecoin transactions for Base builders, enabling verified payment identities and safer fiat-to-onchain flows. | \$1,500 in credits toward GrailPay’s authentication and verification APIs. | Submit your project details to [support@grailpay.com](mailto:support@grailpay.com). After verifying Base builder eligibility, we will provision \$1,500 in authentication credits to your GrailPay account. | | [Hexens](https://hexens.io) | Security | At Hexens, we provide security audits to protect the future of Web3. We directly secure \$120B+ in assets, working with industry leaders like Lido, EigenLayer, LayerZero, 1inch, Ava Labs, and Polygon. | Hexens will provide a discount of 15% for smart contract audits and 10% for services like pentest’s, and social engineering. Full triage will be provided for our bug bounty \[r.xyz] for 3 months. | Please send your audit request to [alice.rigby@hexens.io](mailto:alice.rigby@hexens.io) or @alicerigby on Telegram. | | [Hypernative](https://www.hypernative.io/) | Security | Hypernative is the leading real-time security and threat prevention platform trusted by over 200 projects—including Ethena, Uniswap, Ethereum Foundation, Morpho, Chainlink, Solana, and Kraken. | Receive a discounted rate for the first year for Hypernative's real-time threat prevention platform. | Email [marshall@hypernative.io](mailto:marshall@hypernative.io) to begin your trial and claim your offer. | | [Immunefi](https://immunefi.com/) | AI & Security | Immunefi — One Platform. Unified Security Operations. Complete Onchain Protection. Over \$180B of user funds protected across 500+ protocols. | 15%+ discount on Immunefi Audits, Audit Competitions, Vulnerability Detection/PR Reviews, Onchain Monitoring/Threat Prevention, Cloud Based Formal Verification, Brand Protection and Bug Bounty. | Please fill in this [form](https://calendly.com/d/cwsd-82q-rpj), and our Sales Team will review your submission and contact you shortly. | | [Layer3](https://layer3.xyz) | Ecosystem Growth | Acquire high-quality users through campaigns that drive meaningful on-chain engagement. | 15% discount on all campaign fees for Base Builders! | Contact: [https://t.me/justkhoo5](https://t.me/justkhoo5)
or
Fill out our enquiry form: [https://shorturl.at/NbI67](https://shorturl.at/NbI67) | | [MCA - MultiChain Advisors Inc.](https://www.multichainadv.com) | Consulting Firm | MCA is one of the top growth firms specalized in Marketing, GTM, ICOs, Partnerships, Capital Markets (Raise & Tokenomics), PR/Media, KOLs, and more - driving end to end execution from launch to scale. | Happy to provide 10-20% off our different services for the Base ecosystem. | Please fill this form out & include the code "MCA-Base"
[MCA Intake form](https://form.typeform.com/to/SU7MlYo1) | | [Meow](http://meow.com/) | Treasury Management & Yield | Meow helps web3 teams earn yield, send/receive USDC, and automate treasury via FDIC-insured accounts with free USDC transactions on Base—no wallets, prefunding, or exchange risk. | Only Base ecosystem projects (and select VC portfolios like a16z) get up to 3.5% interest on checking. Others get 0% and must use external funds for yield. | Sign up via [https://app.meow.com/signup?referral=Base](https://app.meow.com/signup?referral=Base)
Or list “Base Ecosystem” under “How did you hear about us?” during signup.
For intros, DM @dustinmeow on Telegram. | | [Neynar](https://neynar.com) | Social and crypto infrastructure | Infrastructure to build easily in crypto and on social protocols like Farcaster. | 100% off for first month of Starter tier, only new customers are eligible. | Email [team@neynar.com](mailto:team@neynar.com) with what you're building on Base to get the coupon code | | [NodeOps](https://nodeops.network/) | Cloud & Infrastructure | NodeOps Cloud is a permissionless infrastructure platform that delivers the most affordable compute power on the market. | 500 USD NodeOps Cloud Credit per Project (No-Questions Asked) & 10,000 USD and above NodeOps Cloud Credit per Project (After validation) | Users must complete the BuildOnNodeOps Grant Form, and NodeOps’ BD team will contact them if their projects require assistance. Alternatively, projects can reach out to the NodeOps Team via [business@nodeops.xyz](mailto:business@nodeops.xyz).
[Application form](https://forms.zohopublic.in/parthnod1/form/BuildOnNodeOpsGrantProgram/formperma/A0j6q-ChMzEUldiqrQNDrkyk7iIaIwmb_6swORtbUqc) | | [Notion](https://www.notion.com) | AI, Productivity | The AI workspace that works for you. One place where teams find every answer, automate the busywork, and get projects done. | Startups / Projects get 6 months free of Notion Business with AI included. | To learn more and redeem the exclusive offer, visit [https://ntn.so/base](https://ntn.so/base) | | [Octane Security](https://www.octane.security/) | Security/Developer Tooling | Octane is an AI-powered smart contract security tool that integrates into your CI/CD pipeline, auto-generates code diffs, fixes and catches bugs missed in traditional audits! | We can offer 15% discount for Octane services. | Book an intro call: [https://calendly.com/d/cqyp-gjr-rvq/octane-introduction](https://calendly.com/d/cqyp-gjr-rvq/octane-introduction)
YOU MUST SPECIFY YOUR COMPANY NAME AND \[Base Builder] WHEN BOOKING | | [OkHi](https://www.okhi.com) | Compliance, Fraud, Credit | Collect digital Proof of Address for your customers anywhere in the world. Integrate our SDK into your mobile app to strengthen compliance, mitigate fraud and improve credit. | 15% off any OkHi service for 1 year | 1. Register your info at okhi.com/enquiry
2. Mention the Base Services Hub in the "Tell us how we can help you" box
3. We'll get in touch for a demo | | [Onchain](https://onchain.org) | Research | Onchain's Research-as-a-Service delivers onchain insights via custom reports, ecosystem analysis & dashboards, guiding protocols, builders, VCs & startups in the Base Ecosystem to informed decisions. | 10% off total services - $1-$10,000 15% off total services - $10,001-$25,000 20% off total services - \$25,001+ | Discounts apply only to research services that center on your core company or BASE-bound contract. Projects outside that scope aren’t eligible. If you’re actively building and supporting the Base ecosystem and fit these criteria, request your discount via the [Onchain research discount form](https://docs.google.com/forms/d/e/1FAIpQLScdJLiTU-RWsNMwsenpmRYWihlVNoTh5weSoB1cXqjwKDpGhg/viewform). | | [OpenCover](https://opencover.com) | Insurance (or alternatively Security) | OpenCover is the #1 onchain cover provider on L2 (crypto-native insurance) used by wallets, platforms and protocol teams to cover their users against protocol and transaction risk programmatically. | Waived protocol or transaction insurance/cover setup fees, including listing, underwriting capital provision and API access (typically \$5,000). | Apply on the [OpenCover for Base builders page](https://opencover.com/base-builders) or contact Jeremiah: [https://t.me/itsjeremiahs](https://t.me/itsjeremiahs) | | [Paladin](https://paladinsec.co/) | Security & Audits | Smart contract security firm. 500+ audits, \$10B+ TVL protected. Audits, pen testing, monitoring, and incident response across EVM, Solana, and Move. | 10% off all security engagements — audits, pen testing, and monitoring exclusively for teams building on Base. | Submit your RFQ at paladinsec.co
Mention you're building on Base in your submission
DM @zabi\_w on Telegram to confirm (we'll apply the discount before scoping) | | [Privy](https://www.privy.io/) | Wallets | Privy powers user onboarding and wallet infrastructure for many of the most popular products built onchain. | 25% off of Privy's listed pricing tiers for your first three months | Reach out to [base@privy.io](mailto:base@privy.io) with your Privy appID and a brief description of what you're building to redeem offer. | | [Proof of Play](https://docs.proofofplay.com/services/vrng/about) | Infrastructure | Proof of Play helps builders create high-performance, serverless apps and games that can be extended or remixed by anyone. | 20% off at 500K/mo+ transactions | Message @adamfern on Telegram | | [Pyth Data Association](https://www.pyth.network/) | Oracle’s / Data Infrastructure | Get pure, real-time market data across every asset class—with more symbols and coverage than anywhere else. | Up to 2 months of free access to the Pyth Pro package (\$10,000 monthly). | Register your interest on the [Pyth Pro interest form](https://tally.so/r/3xG8E5)
Ensure to fill "Base Services Hub" to the 'How did you hear about Pyth?' question to benefit from such offer. | | [QuickNode](https://www.quicknode.com/) | Infrastructure and Developer Tooling | QuickNode is the leading blockchain infrastructure platform for high-performance teams, offering 99.99% uptime, ultra-low latency, and a complete suite of blockchain data tools. | A one-time \$300 credit | Apply through the [QuickNode Startup program](https://www.quicknode.com/startup) and mention Base Services Hub in the last question before submitting. You’ll receive an email upon approval. | | [Ratio1](https://ratio1.ai/) | AI Tools, Decentralized Hosting, Computing\&Storage | Ratio1 is a meta-OS for AI - decentralized, scalable & trustless - that turns idle devices into compute power, replacing traditional cloud infrastructure. | 1 year of free decentralized hosting, compute, and hands-on engineering support to help Base builders deploy, scale, and operate seamlessly. | 1) Apply: [https://ratio1.ai/grants/ratio1-x-base-grants-program](https://ratio1.ai/grants/ratio1-x-base-grants-program)
Include repo or other links confirming you’re building on Base.
2) Get selected: We’ll review and email results.
3) Interview
4) Get support & free service for 1 year | | [Runtime Verification](https://runtimeverification.com) | Security | Runtime Verification secures smart contracts with open-source formal verification and quality assurance tools. Trusted by Lido, Optimism, Uniswap, Solana and more. | FREE Audit Readiness assessment and consultation; 10% off all formal verification and security services; 20% on KaaS - our cloud formal verification platform subscriptions | Choose "Base" on the contact form under ecosystem dropdown menu: [https://amp.runtimeverification.com/](https://amp.runtimeverification.com/)
OR
Reach out to [https://t.me/gregorymakodzeba](https://t.me/gregorymakodzeba) on Telegram or Email: [gregory.makodzeba@runtimeverification.com](mailto:gregory.makodzeba@runtimeverification.com) and mention you are building on Base to activate a discount | | [Security.xyz](https://security.xyz) | Security | Security.xyz is a free open marketplace where onchain builders can easily find vetted, trusted auditors to secure their projects and build with confidence. | Each auditor on base.security.xyz is offering up to \$100,000 in security grants for base builders. | Submit your request for an audit and you'll receive multiple proposals with discounts applied. | | [Slash](https://slash.com) | Banking | Slash provides an all in one banking platform that includes business checking, high yield treasury, high cashback cards, and more. We also support native on/off ramp for USDC on chains like Base. | Founders in the Base ecosystem can bank with Slash for free, and receive up to 2.3% cash back on most categories, up to 3.9% treasury yield, and low off ramp fees | Make an account at [https://app.slash.com/onboarding?invite\_code=BASE](https://app.slash.com/onboarding?invite_code=BASE) to claim the offer. | | [Team Finance ](https://www.team.finance/) | Token Management | The leading token management platform on Base. We offer a full suite of tools, including Liquidity Locks, Team Token Locks, Token Vesting, Token Generation, Staking Pool Creation, and a Multisender. | 20% discount for Team Finance services on Base. | Your discount will be automatically applied when using Team Finance on Base. | | [Token Terminal](https://tokenterminal.com/) | Financial & Protocol Analytics | Token Terminal is a full-stack onchain data platform focused on standardizing financial and alternative data for the most widely used blockchains and decentralized applications. | Token Terminal will offer Base builders a -40% discount on its Data Partnership subscription product. | Apply through the [Token Terminal listings explorer](https://tokenterminal.com/explorer/listings).
All Base builders will need to submit a "proof of deployment on Base". | | [Tokka Labs](https://tokkalabs.com/) | Token Management | Tokka Labs is a DeFi-native prop trading firm offering custom onchain market making across 70+ venues, helping projects grow TVL, volume, and token utility through tailored liquidity strategies. | Priority access to liquidity partnership scoping. Base-native projects are fast-tracked for initial conversations with our team to explore potential liquidity partnerships tailored to their needs | Submit your project to this [form](https://form.typeform.com/to/hz4Nf5zV). | | [Tunnl](https://tunnl.io) | InfoFi, SocialFi | The ultimate growth tool for Web3 projects. Launch a campaign to get quote-tweets from real Web3 creators & grow your social reach! | For Faucet campaigns, we can reduce our standard fee from 20% to 15%. Note our minimum campaign budget is currently \$1,000 (this may be subject to increase due to demand). | Fill out the [Faucet request form](https://forms.gle/jt4t8hTqG2uLgSby7) and our team will be in contact. | | [Validation Cloud](https://www.validationcloud.io/base) | Infrastructure Provider | Validation Cloud is the leading SOC2 Type II Web3 infrastructure platform with 99.99% uptime and ultra-low latency globally. | 2 months free or \$500 credit for Base RPC use in our Node API product (whichever comes earlier). | To redeem this offer, send a partnership contact request at [https://www.validationcloud.io/contact](https://www.validationcloud.io/contact) mentioning that you are building on Base after finding us on the Base Service Hub. | | [Zapper](https://protocol.zapper.xyz/) | Data API | Access portfolio data, token prices, NFTs, and transaction history on Base with a single API. | Free API credits (5,000) and a 15% discount on credit purchases for the Zapper API | Create an account on [Zapper](https://protocol.zapper.xyz/) and use the discount code "BASE15" when purchasing credits. | ## Agencies | Company | Category | Description | Discount | Get In Touch | | :------------------------------------------------------------------------ | :-------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | [1008](https://1008.ventures) | Full Stack Development and Smart Contracts | We help Web3 companies build full-stack solutions—from frontends to backends to smart contracts and deployment. Our team works across DeFi, NFT, and token infrastructure projects. | Free one month of consulting and complimentary independent audit reports for good size projects. (\$5,000 min retainer) | [sahil@1008.ventures](mailto:sahil@1008.ventures) | | [Braille Studio](https://braille.wtf) | Product Design, Branding, Motion/Video, Web Design, Engineering | We design and build products for Based teams, covering brand, web, product, and launch assets, all focused on shipping something people can actually use. | 15% off our services and a free 1-hour mentoring session. (Starts at \$3,000) | [croissant@braille.wtf](mailto:croissant@braille.wtf) or Telegram: [braille\_studio](https://t.me/braille_studio) | | [Builders Garden](https://www.builders.garden/) | Product Studio | Fullstack product studio, specialized in consumer crypto use cases. | 15 mins free review / feedback sessions. | Telegram: @limone\_eth | | [Dacoit Design](https://www.dacoit.design/) | Design Studio | Full-stack crypto-native design studio specializing in brand identity, website design, and product design for Web3 projects. | Complimentary design audit. 20% discount on retainer engagements. (Starting at \$7,500 for a 2-week branding sprint) | Telegram: @karanruparel or Email: [karan@dacoit.design](mailto:karan@dacoit.design) | | [Ethereal Labs](https://www.ethereallabs.io/) | Full Stack Crypto Development Agency | End-to-end Web3 engineering with a focus on bespoke smart-contract architecture, high-performance dApp development, and secure on-chain infrastructure, taking projects from concept to production-ready launch. | 10% discount on services and free initial consultation. | [dev@ethereallabs.io](mailto:dev@ethereallabs.io), Telegram: [ethereallabs](https://t.me/ethereallabs), or X: [@ethereallabs\_](https://x.com/ethereallabs_) | | [ForceField Digital](https://www.forcefield.digital/) | Marketing Agency | ForceField is the operating group and growth partner for Kenetic Capital and a leading venture capital in Web3 with over 300 investments. ForceField is a Web3-native growth partner that delivers real traction. | Base ecosystem members will receive a 20% discount. | [info@forcefield.digital](mailto:info@forcefield.digital) | | [Gloww](https://gloww.design) | Product Design, UX/UI, Branding, and Motion Design | Gloww gives Base builders hands-on product design support, working like an embedded founding designer. Focus is on shaping the product, polishing the UX, refining the visuals, and shipping high quality interfaces. | Happy to give anyone coming from Base services Hub a 20% off. (\$3,000 min fee) | DM @akshitvrma on Telegram | | [GMGM Media](http://gmgm.media/) | Video Editing | Podcast repurposing, TikToks, interviews, launch / hype videos. | No payment required upfront. | Message @GMGMMedia on TG | | [HeimLabs](https://www.heimlabs.com/) | Fullstack Blockchain Development + Design + DevRel Agency | Delivers end-to-end blockchain engineering — full-stack dApp and miniapp development paired with high-quality DevRel content creation. | Free consultation. 20% off on the first order. Unlimited revisions on design (within reason). Bonus content in the DevRel package. (\$250 min fee) | email: [info@heimlabs.com](mailto:info@heimlabs.com) or Telegram: [xhohenheim](https://t.me/xhohenheim) | | [High Agency](https://highagencydevrel.com/) | Developer relations, developer experience, developer onboarding | We make your Web3 product easier to understand, integrate, and build with. From improving documentation and onboarding to growing engaged communities and creating technical content, we ensure developers can adopt your technology seamlessly. | Free initial consultation where we will identify gaps in developer onboarding experience and points of improvement. | Telegram: @enjojoy | | [Ice Breaker TV](https://x.com/ice_breaker_tv) | Twitter Space Show Host | Twitter space / show / podcast / livestream hosting. | Open to discuss larger package deals for discounts / perks for multiple bookings. (\$300 / hourly show min fee) | Telegram or Discord @ice\_breaker\_tv | | [Jonathan Kramer](https://www.jonathankramer.net/) | Video | Concept development, writing, directing, producing, editing, and much more to help produce the videos of your dreams. | Free consultation. | [KramersEmail@gmail.com](mailto:KramersEmail@gmail.com) | | [Juicebox](https://juicebox.it) | Branding, Product, Motion Design | A creative venture studio that designs brands, products, and experiences that feel native to internet culture, built fast and tastefully. | Free consultation / Lock in for 3 months and get a based 20% off your first month. | [hey@juicebox.it](mailto:hey@juicebox.it) | | [Lampros Tech](https://lampros.tech/?utm_medium=partner\&utm_source=base) | Development & Data Analytics Services Provider | Lampros Tech is a Web3-native technical partner that turns protocol complexity into real products. We design and ship secure smart contracts, governance tools, and data systems across Ethereum and major L2s. | 5-15% discount depending on the duration of the service. (\$5,000 min fee) | [hirangi@lampros.tech](mailto:hirangi@lampros.tech) | | [MarcoV](https://dune.com/Marcov) | Dune Dashboard, Onchain Analysis | I provide onchain data analysis, build Dune dashboards, and write clear, actionable reports based on blockchain data. | 10% on hourly rate. | Telegram: @Marc0\_V or X: [@marcov\_91](https://x.com/marcov_91) | | [Memetic Design](https://memetic.design) | Product & Web Design | We help startups create memorable websites that stand out. We hate templated sites that feel the same – your product deserves a bespoke site that sells the what/why/how of your product story. Website design & dev, product design and dashboards, brand design & launch videos. | Free launch video along with every design project. | X: [@abnux](https://x.com/abnux) or Telegram: @abnux2 | | [pandajackson42](https://x.com/pandajackson42) | Data Analytics | Transform data into actionable decisions and compelling stories: from data dashboards, analytics, to product and GTM strategy execution. | Priority support for Base builders. | DM [pandajackson42](https://x.com/pandajackson42) | | [Paperclip Labs](http://paperclip.xyz/) | Product Design and Development | Since 2021, we’ve helped leading crypto teams and protocols design, build, and ship better products. | Free consultation. | [contact@paperclip.xyz](mailto:contact@paperclip.xyz) | | [Plus1000aura](http://plus1000aura.com/) | Creative Video Partner | We make brand films, launches, fundraise announcements for companies in AI and crypto. | 20% off on all videos. (\$5,000 min fee) | [plus1000aura.com](http://plus1000aura.com/) | | [Rock'n'Block](https://rocknblock.io) | Development Services | Rock’n’Block is a Web3-native dev shop. 🚀 We build first-class Web3 products end-to-end—from research and UX/UI to development and maintenance. | 10% off Rock’n’Block development services + free initial consultation for Base builders. Redeem using promo code BASE10RNB. | Fill out the form at [rocknblock.io/#contact](https://rocknblock.io/#contact) (Include promo code BASE10RNB) | | [Sealaunch Intelligence](http://sealaunch.xyz/) | Onchain Intelligence Advisory, Data Analytics, Custom Dune Dashboards | Onchain intelligence and strategic advisory for crypto companies. We conduct private research to drive growth and revenue decisions and create custom Dune Dashboards. | 15% discount with a minimum three-month engagement. | Fill out this [typeform](https://3spxelklbj0.typeform.com/to/NpNyMq98?utm_source=base) | | [Spotlight Crypto](https://www.spotlightcrypto.xyz/) | Full Stack App Development | We build full stack apps on the frontier of crypto social, from ideation to design to smart contracts to GTM. | 15 mins free review / feedback sessions. | [hello@spotlightcrypto.xyz](mailto:hello@spotlightcrypto.xyz) | | [Tarun Thusu](https://tarunthusu.com/portfolio.pdf) | Product and Brand design | I’m a product and brand designer with over 6 years of experience, helping businesses turn ideas into beautiful and functional digital products. From brand identity to user-focused product design, I build visuals, systems, and experiences that make products feel premium, intuitive, and memorable. | 20% discount and fast delivery of work. | Telegram: @tarunth | | [Vacuumlabs](http://www.vacuumlabs.com/) | Software house / Development + design studio | Dev studio with 13 years in Fintech and 7 years in Crypto, we can help augment teams with experienced devs who are top talent from Central Europe; or we can design build and test entire apps in end2end delivery. Our services range from building a neobank MOX for Standard Chartered, to building decentralized onchain apps. | 10% discount for Base ecosystem clients. (Rates 400-1000 EUR/MD based on seniority) | [peter.hucik@vacuumlabs.com](mailto:peter.hucik@vacuumlabs.com), TG: @hukusik, or TG: @PenguDamien | | [Modjo](https://www.modjo.me/) | Growth & Marketing | AI-native growth collective for onchain and tech companies. 70+ companies served across crypto, DeFi, AI, and fintech. We build and run full growth systems with specialists + AI automation. | Free growth diagnostic call for Base builders. 20% off our Commando Sprint (6-8 week growth validation — ICP, channels, messaging, playbook). | Fill out the [form](https://www.modjo.me/forms) or email [alexy@modjo.me](mailto:alexy@modjo.me) and mention "Base Builder" in your message. | # Connect to Base Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/connect-to-base Network details for Base Mainnet and Base Sepolia: RPC endpoints, chain IDs, and block explorers. Base is a standard EVM chain, so any Ethereum tool, wallet, or library works unchanged. Just point it at the network details below. ## Network Details | | Base Mainnet | Base Sepolia (testnet) | | -------------- | ------------------------------------ | ---------------------------------------------------- | | RPC endpoint | `https://mainnet.base.org` | `https://sepolia.base.org` | | Chain ID | `8453` | `84532` | | Currency | ETH | ETH | | Block explorer | [basescan.org](https://basescan.org) | [sepolia.basescan.org](https://sepolia.basescan.org) | ## Next Steps# Static Docs Files Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/docs-llms Use llms.txt and llms-full.txt to give AI assistants access to Base documentation. ## Documentation Index Fetch the complete documentation index at: [https://docs.base.org/llms.txt](https://docs.base.org/llms.txt) Use this file to discover all available pages before exploring further. ## Full Documentation File If your AI tool doesn't support MCP yet, you can use a static documentation file instead. This gives your AI assistant the entire Base documentation as one text file. Fund an address to start transacting. Send your first transaction on Base. The static `llms-full.txt` file is a snapshot and may not include the latest updates. Use [MCP](/get-started/docs-mcp) when possible for always-current docs. ### Setup with Cursor [Cursor](https://cursor.com/) is an AI-powered code editor built as a fork of VS Code with features like AI code completion and natural language editing.### Setup with Claude Code [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is an agentic coding tool that lives in your terminal and understands your codebase. Go to **Settings** > **Features** > **Docs**. Click **Add new doc** and paste: `https://docs.base.org/llms-full.txt` Use `@docs -> Base` in your AI chat to reference the documentation. # MCP Server Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/docs-mcp Connect your AI coding assistant to Base documentation using Model Context Protocol for real-time access. [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI assistants securely access external data sources. The Base MCP server connects your AI coding assistant directly to our documentation, giving it live access to search and retrieve the exact information you need in real time. ## Setup with Cursor [Cursor](https://cursor.com/) is an AI-powered code editor built as a fork of VS Code with features like AI code completion and natural language editing. Download the static documentation file from: [https://docs.base.org/llms-full.txt](https://docs.base.org/llms-full.txt) Save the file in your project directory or a known location on your system. Use the `/read` command or drag and drop the file path to include the documentation in your conversation. Claude Code will then have access to the full Base documentation for that session. ## Setup with Claude Code [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is an agentic coding tool that lives in your terminal and understands your codebase. In Cursor, open **Settings** and navigate to **Tools & MCP**, then click **Add MCP Server**. In the `mcp.json` configuration file, add: ```json mcp.json theme={null} { "mcpServers": { "base-docs": { "url": "https://docs.base.org/mcp" } } } ``` Save the file and restart Cursor to apply the changes. Your AI assistant can now access Base docs in real time. Try asking: "How do I deploy a smart contract on Base?" # Get Funds Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/get-funds Fund an address on Base: withdraw from a Coinbase account, bridge from another chain, or use a testnet faucet. You need a small amount of ETH on Base to pay for gas, plus whatever assets your app uses (USDC is the most common). How you fund depends on where your assets are. ## Mainnet * **From a Coinbase account:** withdraw ETH or USDC and select **Base** as the network. Often the fastest path, no bridge required. * **From another chain:** see [Bridge to Base](/base-chain/network-information/ecosystem-bridges) for routes from Ethereum, Solana, and Bitcoin. ## Testnet (Base Sepolia) * Get free test ETH and USDC from the [Base faucets](/base-chain/network-information/network-faucets). * Test USDC is also available from the [Circle faucet](https://faucet.circle.com). Select **Base Sepolia**. ## Next Steps Run the following command in your terminal: ```bash Terminal theme={null} claude mcp add --transport http base-docs https://docs.base.org/mcp ``` Check that the server was added successfully: ```bash Terminal theme={null} claude mcp list ``` Launch Claude Code and start asking questions. Try: "How do I deploy a smart contract on Base?" # Integrate DeFi Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/integrate-defi Add trading, direct lending, collateralized borrowing, or a vault-based earn product to your app with third-party protocols on Base. Connect your app to third-party DeFi protocols on Base. Let users trade tokens, manage direct lending positions, borrow against collateral, or deposit once into a vault-based earn product while signing every transaction from their own wallet. ## Demo Send your first transaction on Base. Accept USDC from your users. The demo above is mock only. If you want to see onchain demos on Vibenet, head to [Base chain demos](https://chain.base.org/demos). ## Guides# Tokenize Assets Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/issue-rwa Represent and operate real-world assets on Base with the B20 Asset standard: configurable precision, issuer roles, holder controls, and distributions through one ERC-20-compatible surface. Represent a real-world asset with the [B20 Asset standard](/specifications/b20/specification-overview#asset). Configure precision and issuer roles, distribute units, restrict eligible holders, and run distributions through one ERC-20-compatible surface built into Base. The guides below use a stock token as the worked example; the same flows apply to other asset types. Add token swaps with executable routes from the 0x Swap API. Supply USDC to a money market and manage the position directly. Borrow USDC against WETH and monitor liquidation risk. Give users a one-deposit vault experience with variable onchain yield. Real-world asset (RWA) tokenization is one of many use cases for the [B20 Asset standard](/specifications/b20/specification-overview#asset). The examples on this page use a stock token for illustration; the same flows apply to other asset types. Tokenized securities examples shown for illustration. Base is a general-purpose blockchain; issuance and compliance are the responsibility of the issuer under applicable law. ## DemoThe demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. ## Guides# Issue a Stablecoin Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/issue-stablecoins Run a fiat-backed stablecoin on Base with minting, compliance, and reconciliation built into the chain. Issue a fiat-backed stablecoin on Base with [B20](/specifications/b20/specification-overview), Base's native token standard. Minting, redemption, compliance controls, and reconciliation ship with the chain, so there's no custom contract to build or audit, and it's fully ERC-20 compatible. ## Demo Configure a B20 Asset token for one class of units. Distribute units to multiple approved holders in one batch. Gate issuance and transfers with a shared allowlist. Burn units after a holder is denied by the sender policy. Publish an onchain notice and distribute additional units. Update displayed balances without migrating holders. Halt transfers during an incident while issuance stays available. The demo uses a local browser-generated account to submit real transactions on **Base Vibenet**. If Vibenet or its B20 features are unavailable, it automatically switches to an illustrative offline version. ## Guides# Make a Transaction Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/make-a-transaction Send your first transaction on Base with viem: connect, sign, and confirm in seconds for a fraction of a cent. Base uses the same transaction model as Ethereum, so any EVM library works. Here's a minimal send using [viem](https://viem.sh). ## Send a Transaction ```ts send.ts lines wrap expandable theme={null} import { createWalletClient, http, parseEther } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { base } from 'viem/chains'; const account = privateKeyToAccount('0xYourPrivateKey'); const client = createWalletClient({ account, chain: base, transport: http('https://mainnet.base.org'), }); const hash = await client.sendTransaction({ to: '0xRecipientAddress', value: parseEther('0.001'), }); console.log(`Sent, view at https://basescan.org/tx/${hash}`); ``` Create a fiat-backed token in one call. Issue new tokens as reserves grow. Retire tokens on redemption. Limit transfers to approved accounts. Stop one address on a compliance hold. Reclaim and reissue a blocked balance. Halt transfers, mints, or burns. Match onchain activity to your books. Never hardcode or expose a private key. `'0xYourPrivateKey'` is a placeholder. Load the key from an environment variable or a secrets manager, keep it server-side, and never commit it to source control. Transactions confirm in under a second on Base thanks to [Flashblocks](/specifications/transactions/transaction-ordering#flashblocks), and typically cost a fraction of a cent in gas. ## Next Steps# Resources for AI Agents Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/resources-for-ai-agents Base-first resources for AI agents, including docs indexes, MCP access, skills, and recommended starting points Launch a fiat-backed token in one call. Accept USDC with one-tap checkout. Fetch the complete documentation index at [https://docs.base.org/llms.txt](https://docs.base.org/llms.txt). Use this file to discover all available pages before exploring further. Use this page as the starting point when you want an AI assistant to build on Base. It covers three ways to give an assistant Base context: static docs files, a live MCP connection, and installable skills. You'll also find recommended entry points for common workflows. ## Quick Setup Get your agent connected to Base docs in one command. Pick the method that fits your tool. ### MCP Server A live connection lets your assistant search and read Base docs on demand, so it always has current information.```sh Claude Code theme={null} claude mcp add --transport http base-docs https://docs.base.org/mcp ``` ```sh Codex CLI theme={null} codex mcp add base-docs --url https://docs.base.org/mcp ``` ```json Cursor / manual config theme={null} { "mcpServers": { "base-docs": { "url": "https://docs.base.org/mcp" } } } ``` See the [full MCP setup guide](/get-started/docs-mcp) for Cursor, Windsurf, and other editors. ### Static Docs Files Use these when you want to load context in a single fetch rather than maintaining a live connection. | File | What it contains | When to use it | | ------------------------------------------------------ | --------------------------------------- | ------------------------------------------------ | | [`llms.txt`](https://docs.base.org/llms.txt) | Page index with titles and descriptions | Discovering which docs exist before going deeper | | [`llms-full.txt`](https://docs.base.org/llms-full.txt) | Complete documentation in one file | Giving an assistant broad context in one shot | Every docs page is also available as plain Markdown. Append `.md` to any URL: ```txt Markdown URL theme={null} https://docs.base.org/get-started/resources-for-ai-agents.md ``` ### Skills Skills are installable agent workflows for common Base tasks, including connecting to Base, deploying contracts, running a node, and more. They give your assistant step-by-step procedural guidance instead of requiring it to piece together docs on its own. ```sh Terminal theme={null} npx skills install base/skills -g ``` Browse available skills in the [Base skills repository](https://github.com/base/skills). ## Recommended Starting Points Once your agent has docs context, point it at the section that matches what you're building: | What you're doing | Start here | | --------------------------------------- | ------------------------------------------------------------------------ | | Connecting an assistant to live docs | [MCP server](/get-started/docs-mcp) | | Loading docs as static files | [Static docs files](/get-started/docs-llms) | | Adding payments or onchain transactions | [Make x402 payments](/build-on-base/accept-payments/call-a-paid-service) | | Deploying contracts | [Deploy on Base](/get-started/make-a-transaction) | | Building an app on Base | [Build a Base app](/build-on-base/overview) | ## Example Prompts Copy these into your assistant to test that everything is working: * "Deploy my ERC-20 contract to Base Sepolia and verify it on Basescan." * "Add Sign in with Base to my Next.js app using wagmi." * "Set up a wallet for my AI agent that can hold USDC and sign transactions autonomously." * "Create an app with a USDC payment flow." * "Build an agent that pays for API requests using the x402 protocol." # SDKs & APIs Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/get-started/sdks-and-apis Choose the Base SDK, API, CLI, or MCP integration that matches what you are building. Use these references when you know which interface your application needs. Start with the SDKs & APIs overview to compare every surface, or jump directly to accounts, AI agents, local development, or chain RPC methods. ## Choose an Integration Surface## Call the Chain Compare the wallet and payments SDK, Base MCP, the Base Chain API, and command-line tooling. Add universal sign-in, passkey-backed accounts, and one-tap USDC payments to your app. Give AI assistants access to wallet actions, swaps, signatures, and x402 payments. Build and test Base-native contracts locally with Base's Foundry toolchain. # Basename Transfer Guide Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/basenames/basename-transfer Step-by-step guide for Base App users to transfer their Basenames to new wallet addresses. ## Transferring Your Basename to Your New Wallet If you have an existing Basename you'd like to use, you'll need to transfer it to your new wallet address. We do not allow import or linking of pre-existing Basenames without transfer at this time. ### Before You Start Choose between standard block data and 200 ms Flashblock preconfirmations. Read chain state, estimate gas, send transactions, query logs, and subscribe to events. Use preconfirmation-aware methods and streams for sub-second application feedback. Trace transactions and replay blocks when debugging contract execution. Find RPC, data, security, and other service providers that support Base builders. **Your new wallet address is ready**: We've automatically copied your new wallet address to your clipboard. You'll paste this when prompted. **Make sure to use the Basenames UI to send Basenames properly, sends on platforms like OpenSea will only transfer the NFT.** ### Step-by-Step Transfer Process 1. **Navigate to Basenames** * Go to [base.org/names](https://base.org/names) * Sign in with your current wallet (the one that owns the Basename) 2. **Access Your Basenames** * Click **"My Basenames"** in the top right corner * Find the Basename you want to transfer 3. **Start the Transfer** * Click the **three dots** next to your Basename * Select **"Transfer name"** 4. **Enter Your New Address** * When prompted for the destination address, **paste your new wallet address** (Ctrl/Cmd + V) * The address is already on your clipboard 5. **Complete the Transfer** * Sign all **four transactions** in sequence: * Transfer token ownership * Transfer management rights * Change address resolution * Send the NFT * Each transaction updates a different aspect of your Basename ownership ### After the Transfer Once the transfer is complete, you'll need to activate your Basename on your new wallet: 1. Switch to your new wallet 2. Go to [base.org/manage-names](https://base.org/manage-names) 3. Sign in with your new wallet 4. Find your transferred Basename and click the three dots 5. Select **"Set as primary"** and sign the transaction**For the new owner to use the basename they will need to confirm by setting it as their primary name.** ### What This Transfer Includes When you complete all four transactions, you're transferring: **Transfer token ownership** - transfers ownership of the Basename token and associated permissions.\ **Transfer management** - transfers ability to manage and update profile records.\ **Change address resolution** - Basename will resolve to a new address. Transferring all 3 to the same address will fully transfer ownership of the Basename to that address. ### Need Help? If you encounter any issues during the transfer process, make sure: * You're using the correct wallet (your old one) to initiate the transfer * You've pasted the correct new wallet address * You're completing all four transactions in the sequence Your Basename will be fully functional on your new wallet once the transfer and primary name setup are complete. # Basenames FAQ Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/basenames/basenames-faq Frequently asked questions on basenames. ## FAQ ### 1. What Are Basenames? [Basenames](https://base.org/names) are a core onchain building block that enable builders to establish their identity on Base by registering human-readable names for their wallet address(es). They are fully onchain, built on the same technology powering ENS names and deployed on Base. These human-readable names can be used when connecting to onchain apps, and sending and receiving on Base and any other EVM chain. Get your Basename at [base.org/names](https://base.org/names). ### 2. What Are the Basename Registration Fees? Basenames are priced based on name length, and are designed to be globally accessible. Annual registration fees are as follows: | Letters | Annual fee | | ------- | ---------- | | 3 | 0.1 ETH | | 4 | 0.01 ETH | | 5-9 | 0.001 ETH | | 10+ | 0.0001 ETH | ### 3. How Do I Get a Free or Discounted Basename? You can get one free Basename (5+ letters) for one year if you meet any of the below criteria: * [Coinbase Verification](http://coinbase.com/onchain-verify) * [Summer Pass Level 3 NFT](https://wallet.coinbase.com/ocs) * [Buildathon participant NFT](https://onchain-summer.devfolio.co/) * [base.eth NFT holder](https://opensea.io/collection/base-org-base-eth) * cb.id username (acquired prior to Fri Aug 9, 2024) * [BNS name owner](http://basename.app) - free 4+ letter name (basename.app) An equivalent-value discount of 0.001 ETH will be applied if registering a shorter name, or registering for more than 1 year, with the exception of the BNS name owner discount (valued at 0.01 ETH per unique address). You will need to pay the standard registration fees if you wish to keep your Basename after your initial discount has been fully applied. Discounts are only applied once, and are limited to one per address. Even if you meet multiple criteria, you will only be eligible for a single discount on one Basename. If you satisfy multiple criteria, we will automatically apply the highest-value discount to your registration. We are always looking to add more discounts. If you or your project have ideas for more discounts, please reach out. ### 4. Why Is There an Auction at Launch, and How Does It Work? Upon initial launch, there will be a temporary premium placed on all Basenames in the form of a Dutch auction, to ensure a fair and quality distribution of names, and to maximize everyone's chance of getting a name they like without being outcompeted by bots. The premium will start at 100 ETH and decay exponentially over the course of 36 hours. Premiums will be added on to the total registration cost of a Basename. Please note: the premium is intentionally designed to be high so that names can't be instantly bought by bots or traders, and can instead enable fairer access and price discovery for the general public. ### 5. Do I Have to Pay Gas to Register a Basename? If registering with a Base Account, registrations will be gasless, sponsored by Base. ### 6. How Long Can I Register a Basename For? There is no limit to registration length, but there is a minimum of 1 year. ### 7. How Can I Use My Basename? You can use your Basename across apps in the Base ecosystem, starting with base.org, Onchain Registry, and Onchain Summer Pass. You can also use it for sending and receiving on Base and other EVM chains. ### 8. Is My Profile Information Published Onchain? Basenames are fully onchain, and therefore any information you publish is recorded onchain, requires a transaction, and will be broadly composable with the rest of the ecosystem. Please do not publish any information you do not wish to be onchain. ### 9. How Do I Set My Basename as My Primary Name for My Address? You can set your Basename as your primary name through Profile Management. Setting your Basename as your primary name will display it on any wallet or app that has added support for Basenames. **To set a basename as your primary name:** * Navigate to [My Basenames](https://www.base.org/manage-names) * Sign in with the wallet that now owns the basename * Click the three dots of the basename you want to set as a primary name * Click "Set as primary" and sign the transaction ### 10. How Do I Transfer My Basename to Another Address? You can transfer your Basename to another address through Profile Management:**Make sure to use the Basenames UI to send Basenames properly, sends on platforms like OpenSea will only transfer the NFT.** Transfer token ownership - transfers ownership of the Basename token and associated permissions.\ Transfer management - transfers ability to manage and update profile records.\ Change address resolution - Basename will resolve to a new address.\ Transferring all 3 to the same address will fully transfer ownership of the Basename to that address. Step by step: * Navigate to [base.org/names](http://base.org/names) * Sign in with wallet that owns the basename * Click "My Basenames" in the top right corner * Click the three dots of the basename you want to transfer and click transfer name * Paste the ENS or address of the wallet you want to transfer the basename to * Proceed to sign all four transactions to properly update the basename address, ownership, and profile records. The last transaction will be sending the NFT.**For the new owner to use the basename they will need to confirm by setting it as their [primary name](#9-how-do-i-set-my-basename-as-my-primary-name-for-my-address).** ### 11. What Happens if I Forget to Renew My Basename? If you forget to renew your Name, it will enter a grace period of 90 days, during which you can still renew it. If not renewed during this period, the Basename will become available for others to register. ### 12. What Happens if a Basename Is Not Renewed During the Grace Period? If a Basename is not renewed after the 90 day grace period, it will be subject to a [temporary premium](https://support.ens.domains/en/articles/7900612-temporary-premium) in the form of a Dutch auction. This premium starts at 100ETH and will decay exponentially over the course of 21 days. ### 13. Can I Link Multiple Addresses to My Basename? Currently, only one address at a time can be linked to a Basename. However, we plan to support multi-address linking in the future. ### 14. I Am a Builder. How Do I Integrate Basenames to My App? If you're a builder looking to integrate Basenames into your app, follow the [Basenames + Wagmi tutorial](/sdks/base-account/framework-integrations/wagmi/basenames) to get started. If you have ideas for new features or badges that you'd like to integrate with Basenames, we'd love to [hear from you](https://app.deform.cc/form/b9c1c39f-f238-459e-a765-5093ca638075/?page_number=0). ### 15. How Do I Get a Basename for My App or Project? You can register a Basename for your app just like any other Basename. If a Basename for your app or project is not available, there is a good chance it was reserved. Please reach out to our team or fill out this [form](https://app.deform.cc/form/20372eb6-ec97-4d37-967f-d36f4b7f4eb2) and we will reach out with instructions. ### 16. How Are Basenames Built? Basenames are built using the Ethereum Name Service (ENS) protocol, leveraging its decentralized architecture to ensure secure and efficient name resolution. ### 17. Do Basenames Work on Different Chains? Yes, your Name will work on any chain as long as the app is ENSIP-10 compliant. Note that when sending money or interacting across different chains, you should ensure the receiving platform supports ENS. # Contribute to the Base Account Docs Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/contribute/contribute-to-base-account-docs How to contribute new features, content, and updates to the Base Account documentation while keeping it consistent. This guide is intended for all contributors who are adding new features, content, or making updates to the Base Account documentation. Following these guidelines ensures consistency and maintains the documentation structure, making it easier for developers to find information.**Why Documentation is Important** Good documentation significantly accelerates developer adoption. Focus on creating content that helps developers understand and implement Base Account features efficiently, while maintaining the documentation's structural integrity. ## Documentation Structure Guidelines ### Core Principle: Maintain Existing Structure The Base Account documentation is organized into the following main sections: 1. **Introduction** 2. **Quickstart** 3. **Guides** 4. **Framework Integrations** 5. **Reference** 6. **More** 7. **Basenames** 8. **Contribute****Do not create new top-level sections** All new content must fit within these existing sections. ### Section Purpose and Content Placement When adding new content, determine the appropriate section based on the following criteria: #### Introduction * High-level explanatory content about what Base Account is and its core value proposition * Update only when there are fundamental changes to the product positioning or capabilities #### Quickstart * End-to-end guides for getting developers up and running quickly * Should remain focused and concise * Covers different integration paths (web, web with React, mobile) #### Guides * Step-by-step tutorials for specific tasks and implementation scenarios * Includes guides for authentication, payments, and UX improvements * Covers features like: * User authentication * Accepting payments and recurring payments * Batch transactions * Paymasters and gas sponsorship * Sub Accounts * Spend Permissions * Name guides clearly with action-oriented titles (e.g., "Accept Payments" rather than "Payments Guide") #### Framework Integrations * Detailed integration guides for specific frameworks and libraries * Subsections organized by framework: * **Wagmi/Viem**: Setup, batch transactions, Basenames, and other use cases * **Privy**: Setup, authentication, wallet actions, and sub-accounts * **CDP**: Coinbase Developer Platform integration * **RainbowKit**: Integration with RainbowKit * Each framework section should cover setup and common use cases #### Reference * Comprehensive technical documentation of APIs, methods, components, and configurations * Structured reference material rather than tutorial content * Include parameter descriptions, return values, and usage examples * Organized by component: * **Account SDK**: Core functions, Spend Permission utilities, Base Pay, subscriptions, and Prolink utilities * **Provider**: Methods (RPC methods like `wallet_connect`, `wallet_sendCalls`, etc.) and Capabilities * **UI Elements**: Base Pay Button, Sign In With Base Button, brand guidelines * **Onchain Contracts**: Spend permissions, smart wallet, and Basenames contracts #### More * Additional resources and supplementary documentation * Troubleshooting guides (popups, gas usage, unsupported calls, simulations, wallet library support) * Telemetry information * Migration guides #### Basenames * Documentation specific to Basenames functionality * FAQs and common questions * Basename transfer documentation #### Contribute * Information for contributors to the Base Account project * Security and bug bounty information * Update when contribution processes change**Avoiding Subsection Proliferation** * **For Guides**: Keep all guides at the same level under the Guides section * **For Reference**: Organize by component or feature, not by use case * **For Framework Integrations**: Keep framework-specific content within its respective framework subsection * When tempted to add a new subsection, consider if the content could be reorganized to fit existing sections * Use cross-referencing between related content rather than creating new organizational structures ## Documentation Style Guidelines ### Writing Style 1. **Be concise**: Use simple, direct language. Avoid unnecessary words. 2. **Consistency**: Maintain consistent terminology throughout documentation. 3. **Persona-focused**: Think about the persona of the reader and write accordingly. 4. **Happy Path**: Focus on the happy path, but don't forget to mention the alternative paths. 5. [**AI-friendly**](#ai-friendly-writing-tips): Write in a way that is easy for AI to understand and follow.**Make sure to review any AI generated content** If you use AI to generate content: * Make sure to review it carefully before submission. * Make sure that the content follows the guidelines in this document. * Make sure that the content is easy for AI to understand and follow. ### AI-friendly Writing Tips * Make sure you use explicit language in your file names, headings, and content. * Make active linking references to the relevant guides and examples. * Use bulleted lists for steps or options. * Explicitly name and reference the libraries you are using. * Use code blocks to highlight code. * Use semantic urls that make sense even without context. Avoid abbreviations.**Think like a Large Language Model** When writing documentation, think about how a Large Language Model would understand the content. You should continuously ask yourself: * "Would a Large Language Model be able to understand this content?" * "Would a Large Language Model be able to follow this content?" * "Would a Large Language Model be able to use this content?" If you can't answer yes to all of these questions, you need to rewrite the content. ### Formatting 1. **Markdown usage**: * Use proper heading hierarchy (# for main titles, ## for section headings, etc.) * Use code blocks with language specification (\`\`\`javascript) * Use tables for parameter references * Use bulleted lists for steps or options 2. **Code examples**: * Include complete, working code examples * Comment code thoroughly * Follow the project's coding style guide ### Abbreviations and Terminology 1. **First reference**: The first time you use an abbreviation or technical term, spell it out followed by the abbreviation in parentheses. Example: "Account Abstraction (AA)" 2. **Consistency**: Use the same term for the same concept throughout the documentation 3. **Technical Reference**: Keep the guides and examples to a minimal size. Put the comprehensive technical details in the Technical Reference section. ## Review Checklist Before Submission * [ ] Content fits within existing structure * [ ] No new top-level sections created * [ ] Minimal subsection creation * [ ] Consistent terminology used throughout * [ ] Abbreviations properly introduced * [ ] Code examples are complete and functional * [ ] Cross-references to related documentation added * [ ] Documentation follows style guidelines * [ ] Documentation is written in a way that is easy for AI to understand and follow ## Submission Process 1. Create a documentation Pull Request (PR) to the [repository](https://github.com/base/docs) with your changes 2. Ensure your PR includes updates to all relevant sections and respects the instructions in this guide 3. Request review from the documentation team 4. Address feedback and make necessary revisions 5. Once approved, the PR will be merged and published # Security and Bug Bounty Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/contribute/security-and-bug-bounty Base Account security audits and how to report vulnerabilities through the bug bounty program. ## Security Audits Base Account has undergone multiple security audits. You can find the full list of our audits in the main Base Account repository. [Base Account audits](https://github.com/coinbase/smart-wallet/tree/main/audits) ## Bug Bounty Program The Coinbase/Base Bug Bounty program is a crowdsourced initiative that rewards security researchers for responsibly reporting vulnerabilities in Base's smart contracts and infrastructure. To report a bug, please follow the instructions in the [HackerOne Bug Bounty Program](https://hackerone.com/coinbase?type=team). # Coinbase Developer Platform Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/cdp Build onchain apps supporting both Base Account and CDP Embedded Wallets ## Integrate Base Account with CDP Embedded Wallets Learn how to build an onchain app that seamlessly supports both **existing Base Account users** and **new users** through CDP Embedded Wallets, providing unified authentication and wallet management. ## Overview This integration enables your app to serve two distinct user types: * **Existing Base users**: Connect with their Base Account for a familiar experience * **New onchain users**: Create CDP Embedded Wallets via email, mobile, or social authentication Both user types get the same app functionality while using their preferred wallet type. ## What You'll Build * **Unified authentication flow**: Single sign-in supporting both wallet types * **Automatic wallet detection**: Smart routing based on user's existing wallet status * **Consistent user experience**: Both wallet types access the same app features ## Prerequisites * Node.js 18+ installed * React application (Next.js recommended) * [CDP Portal account](https://portal.cdp.coinbase.com/) with Project ID * Basic familiarity with Wagmi and React hooks ## Installation Install the required packages for both CDP Embedded Wallets and Base Account support: ```bash Terminal theme={null} npm install @coinbase/cdp-core @coinbase/cdp-hooks @base-org/account @tanstack/react-query viem wagmi ``` ## Step-by-Step Implementation Since native CDP + Base Account integration is under development, this guide uses a **dual connector approach** where both wallet types are supported through separate, coordinated connectors. You can use the Base Account Wagmi connector alongside CDP's React provider system to create a unified experience that properly handles wallet persistence for both wallet types. ### Step 1: Environment Configuration Create environment variables for your CDP project: ```bash .env.local theme={null} # .env.local NEXT_PUBLIC_CDP_PROJECT_ID=your_cdp_project_id NEXT_PUBLIC_APP_NAME="Your App Name" ``` Get your CDP Project ID from the [CDP Portal](https://portal.cdp.coinbase.com/). ⚠️ **Critical**: Without a valid `NEXT_PUBLIC_CDP_PROJECT_ID`, the app will fail to load with "Project ID is required" errors. Also configure your domain in CDP Portal → Wallets → Embedded Wallet settings for CORS. ### Step 2: Configure wagmi for Base Account Support Set up Wagmi with the Base Account connector (embedded wallets will be handled separately via CDP React providers): ```typescript config/wagmi.ts lines wrap expandable theme={null} // config/wagmi.ts import { createConfig, http } from 'wagmi'; import { base, baseSepolia } from 'wagmi/chains'; import { baseAccount } from 'wagmi/connectors'; // Base Account connector const baseAccountConnector = baseAccount({ appName: process.env.NEXT_PUBLIC_APP_NAME || 'Your App', }); // Wagmi config (only for Base Account - embedded wallets handled by CDP React providers) export const wagmiConfig = createConfig({ connectors: [baseAccountConnector], chains: [baseSepolia, base], // Put baseSepolia first for testing transports: { [base.id]: http(), [baseSepolia.id]: http(), }, }); ``` ### Step 3: Set Up Application Providers Wrap your application with the necessary providers. **Important**: Use `CDPHooksProvider` to properly manage embedded wallet authentication state: ```typescript app/layout.tsx lines wrap expandable theme={null} // app/layout.tsx 'use client'; import { WagmiProvider } from 'wagmi'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { CDPHooksProvider } from '@coinbase/cdp-hooks'; import { wagmiConfig } from '../config/wagmi'; const queryClient = new QueryClient(); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return (); } ``` ### Step 4: Create Unified Authentication Hook Build a custom hook to manage both wallet types. Using `CDPHooksProvider` ensures users get their existing embedded wallets when they sign in again, rather than creating new ones each time. ```typescript hooks/useUnifiedAuth.ts lines wrap expandable theme={null} // hooks/useUnifiedAuth.ts import { useAccount, useConnect, useDisconnect } from 'wagmi'; import { useSignInWithEmail, useVerifyEmailOTP, useIsSignedIn, useEvmAddress, useSignOut } from '@coinbase/cdp-hooks'; import { useState, useEffect } from 'react'; export type WalletType = 'base_account' | 'embedded' | 'none'; export function useUnifiedAuth() { // Wagmi hooks for Base Account const { address: wagmiAddress, isConnected: wagmiConnected, connector } = useAccount(); const { connect, connectors } = useConnect(); const { disconnect: wagmiDisconnect } = useDisconnect(); // CDP hooks for embedded wallet - these work with CDPHooksProvider const { signInWithEmail, isLoading: isSigningIn } = useSignInWithEmail(); const { verifyEmailOTP, isLoading: isVerifying } = useVerifyEmailOTP(); const { isSignedIn: cdpSignedIn } = useIsSignedIn(); const { evmAddress: cdpAddress } = useEvmAddress(); const { signOut } = useSignOut(); const [walletType, setWalletType] = useState {children} ('none'); const [flowId, setFlowId] = useState (''); // Determine which wallet is active and prioritize the active one const address = wagmiConnected ? wagmiAddress : cdpAddress; const isConnected = wagmiConnected || cdpSignedIn; useEffect(() => { if (wagmiConnected && connector?.name === 'Base Account') { setWalletType('base_account'); } else if (cdpSignedIn && cdpAddress) { setWalletType('embedded'); } else { setWalletType('none'); } }, [wagmiConnected, cdpSignedIn, connector, cdpAddress]); const connectBaseAccount = () => { const baseConnector = connectors.find(c => c.name === 'Base Account'); if (baseConnector) { connect({ connector: baseConnector }); } }; const signInWithEmbeddedWallet = async (email: string) => { try { const response = await signInWithEmail({ email }); // Capture flowId for OTP verification if (response && typeof response === 'object' && 'flowId' in response) { setFlowId(response.flowId as string); } return true; } catch (error) { console.error('Failed to sign in with email:', error); return false; } }; const verifyOtpAndConnect = async (otp: string) => { try { // With CDPReactProvider, verifyEmailOTP automatically signs the user in await verifyEmailOTP({ flowId, otp }); return true; } catch (error) { console.error('Failed to verify OTP:', error); return false; } }; const disconnect = async () => { if (wagmiConnected) { wagmiDisconnect(); } if (cdpSignedIn || walletType === 'embedded') { try { await signOut(); } catch (error) { console.error('CDP sign out failed:', error); } } }; return { address, isConnected, walletType, connectBaseAccount, signInWithEmbeddedWallet, verifyOtpAndConnect, disconnect, isSigningIn, isVerifying, }; } ``` ### Step 5: Build Authentication Component Create a component that presents both authentication options: ```typescript components/WalletAuthButton.tsx lines wrap expandable theme={null} // components/WalletAuthButton.tsx 'use client'; import { useState } from 'react'; import { useUnifiedAuth } from '../hooks/useUnifiedAuth'; export function WalletAuthButton() { const { address, isConnected, walletType, connectBaseAccount, signInWithEmbeddedWallet, verifyOtpAndConnect, disconnect, isSigningIn, isVerifying, } = useUnifiedAuth(); const [authStep, setAuthStep] = useState<'select' | 'email' | 'otp'>('select'); const [email, setEmail] = useState(''); const [otp, setOtp] = useState(''); // Connected state if (isConnected && address) { const walletDisplay = { base_account: { name: 'Base Account', icon: '🟦' }, embedded: { name: 'Embedded Wallet', icon: '📱' }, }[walletType] || { name: 'Connected', icon: '✅' }; return ( {walletDisplay.icon}); } // OTP verification if (authStep === 'otp') { return ({walletDisplay.name}{address.slice(0, 6)}...{address.slice(-4)}); } // Email input if (authStep === 'email') { return (setOtp(e.target.value)} placeholder="000000" maxLength={6} className="w-full px-3 py-2 border rounded text-center font-mono" />Check your email
Enter the code sent to {email}
); } // Initial selection return (Create account
setEmail(e.target.value)} placeholder="your@email.com" className="w-full px-3 py-2 border rounded" />); } ``` ### Step 6: Handle Transactions for Each Wallet Type Create a transaction component that adapts to each wallet type: ```typescript components/SendTransaction.tsx lines wrap expandable theme={null} // components/SendTransaction.tsx import { useState } from 'react'; import { parseEther } from 'viem'; import { useSendTransaction, useWaitForTransactionReceipt, useAccount, useSwitchChain } from 'wagmi'; import { base, baseSepolia } from 'wagmi/chains'; import { useUnifiedAuth } from '../hooks/useUnifiedAuth'; export function SendTransaction() { const { address, walletType } = useUnifiedAuth(); const { chain } = useAccount(); const { switchChain } = useSwitchChain(); const [amount, setAmount] = useState(''); const [recipient, setRecipient] = useState(''); const { data: hash, sendTransaction, isPending, error } = useSendTransaction(); const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash }); const handleTransaction = async () => { if (!address || !amount || !recipient) return; try { sendTransaction({ to: recipient as `0x${string}`, value: parseEther(amount), }); } catch (error) { console.error('Transaction failed:', error); } }; // Show different guidance based on wallet type const getTransactionGuidance = () => { switch (walletType) { case 'base_account': return { title: 'Base Account Transaction', description: 'You\'ll be prompted to confirm with your passkey', icon: '🔐' }; case 'embedded': return { title: 'Embedded Wallet Transaction', description: 'Transaction will be signed automatically', icon: '⚡' }; default: return { title: 'Send Transaction', description: '', icon: '💸' }; } }; const guidance = getTransactionGuidance(); if (!address) return null; return (Connect Your Wallet
); } ``` ### Step 7: Complete Your App Put everything together in your main application: ```typescript app/page.tsx lines wrap expandable theme={null} // app/page.tsx 'use client'; import { WalletAuthButton } from '../components/WalletAuthButton'; import { SendTransaction } from '../components/SendTransaction'; import { useAccount } from 'wagmi'; export default function HomePage() { const { isConnected } = useAccount(); return ({guidance.icon}{guidance.title}
{guidance.description}
{/* Network indicator and switch */}Network: {chain?.name || 'Unknown'}{chain?.id !== baseSepolia.id && ( )} {chain?.id !== base.id && ( )}setAmount(e.target.value)} placeholder="0.001" step="0.001" className="w-full px-3 py-2 border border-gray-300 rounded" />setRecipient(e.target.value)} placeholder="0x..." className="w-full px-3 py-2 border border-gray-300 rounded font-mono text-sm" />{error && ()} {isSuccess && hash && (Error: {error.message}
)}✅ Transaction Confirmed!
View on {chain?.id === baseSepolia.id ? 'Sepolia ' : ''}Basescan →); } ``` ## Troubleshooting ### Common Issues **Base Account connector not appearing** * Verify the Base Account SDK, `@base-org/account`, is installed and up-to-date * Check wagmi configuration includes Base Account connector * Ensure app is running on Base or Base Sepolia network **CDP Embedded Wallet authentication failing** * Verify CDP Project ID is correct in environment variables * **Critical**: Add your domains (e.g., `http://localhost:3000`, `http://localhost:3001`) to CDP Portal → Wallets → Embedded Wallet settings → Allowed domains * Ensure all required CDP packages (see above) are installed **New wallet created each time instead of signing into existing wallet** * Ensure you're using `CDPHooksProvider` with proper config in your layout * Verify CDP Project ID is correctly configured * Check that hooks are imported from `@coinbase/cdp-hooks` consistently **Users can't switch between wallet types** * Implement proper disconnect flow before connecting different type * Clear any cached authentication state when switching * Provide clear UI guidance for wallet type selection ## Enhanced Integration Coming Soon We are actively working on native Base Account integration with CDP Embedded Wallets that will enable: * **Unified connector**: Single CDP connector to handle both wallet types seamlessly * **Spend permissions**: Sub Accounts will be able to access parent Base Account balance with limits * **Sub Account creation**: Base Account users will be able to create app-specific Sub Accounts ## Resources * [CDP Embedded Wallets Documentation](https://docs.cdp.coinbase.com/embedded-wallets/welcome) * [CDP React Components Documentation](https://docs.cdp.coinbase.com/embedded-wallets/react-components) * [Base Account Wagmi Setup](/sdks/base-account/framework-integrations/wagmi/setup) * [CDP Portal](https://portal.cdp.coinbase.com/) * [Wagmi Documentation](https://wagmi.sh/) Monitor the [CDP documentation](https://docs.cdp.coinbase.com/) for updates on enhanced Embedded Wallet Base Account integration features. # Auth (Sign in with Base) Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/privy/authentication Manage user authentication with Privy and Base Account Learn how to handle authentication flows with Privy and Base Account, including both Privy-managed authentication and custom backend verification. ## Overview Privy handles the initial authentication flow, managing user sessions and wallet connections. You can also implement additional authentication layers for enhanced security or custom requirements. The code snippets in this guide are based on the following example project:CDP + Base Account Demo
One app supporting both Base Account and embedded wallet users
{isConnected && } ## Authentication Flow Privy manages the primary authentication before users enter your application: ## Custom Authentication For additional security or custom authentication requirements, you can implement backend verification using Sign-In with Ethereum (SIWE) with the Base Account SDK. ### Setup Follow the [Setup](/sdks/base-account/framework-integrations/privy/setup) guide to set up Privy with Base Account. ### Frontend Component (Sign in with Base) We use the [`SignInWithBaseButton`](/sdks/base-account/reference/ui-elements/sign-in-with-base-button) component from the `@base-org/account-ui/react` package to make sure we are following the brand guidelines.![]()
```tsx Authentication Component (components/sections/authentication.tsx) lines wrap expandable theme={null} "use client"; import { useState } from "react"; import { useBaseAccountSdk } from "@privy-io/react-auth"; import { SignInWithBaseButton } from "@base-org/account-ui/react"; export const Authentication = () => { const { baseAccountSdk } = useBaseAccountSdk(); const [loading, setLoading] = useState(false); const [verificationResult, setVerificationResult] = useState ### Using the Authentication Component Add the Authentication component to your page to enable Sign In with Base functionality:(null); const provider = baseAccountSdk?.getProvider(); const handleSignInWithBase = async () => { if (!provider) return; try { setLoading(true); // Get a fresh nonce from backend const nonceResponse = await fetch("/api/auth/nonce"); const { nonce } = await nonceResponse.json(); // Switch to Base Chain await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x2105" }], }); // Connect and authenticate with SIWE const response = (await provider.request({ method: "wallet_connect", params: [{ version: "1", capabilities: { signInWithEthereum: { nonce, chainId: "0x2105", }, }, }], })) as { accounts: { address: string; capabilities: { signInWithEthereum: { signature: string; message: string }; }; }[]; }; const { address } = response.accounts[0]; const { message, signature } = response.accounts[0].capabilities.signInWithEthereum; // Verify with backend const verifyResponse = await fetch("/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address, message, signature }), }); const result = await verifyResponse.json(); setVerificationResult(result); } catch (error) { console.error("Sign in error:", error); } finally { setLoading(false); } }; return ( ); }; export default Authentication; ```{verificationResult && ( ✅ Backend Verified! Address: {verificationResult.address})}```tsx Page Implementation (app/page.tsx) lines wrap expandable theme={null} import Authentication from "@/components/sections/authentication"; export default function Home() { return ( ### Backend Implementation); } ``` ```tsx Alternative: Protected Page (app/dashboard/page.tsx) lines wrap expandable theme={null} "use client"; import { usePrivy } from "@privy-io/react-auth"; import Authentication from "@/components/sections/authentication"; export default function Dashboard() { const { authenticated } = usePrivy(); if (!authenticated) { return ( Base Account with Privy
); } return (Access Required
Please authenticate to access the dashboard.
); } ```Dashboard
Welcome to your authenticated dashboard!
{/* Your protected content here */}**Development Only**: This backend implementation is not production-ready. The nonce management system needs proper persistence and security enhancements for production use. ```ts Nonce Generation (app/api/auth/nonce/route.ts) lines wrap expandable theme={null} import { NextResponse } from 'next/server'; import crypto from 'crypto'; import { nonceStore } from '@/lib/nonce-store'; export async function GET() { try { const nonce = crypto.randomBytes(16).toString('hex'); nonceStore.add(nonce); return NextResponse.json({ nonce }); } catch (error) { return NextResponse.json( { error: 'Failed to generate nonce' }, { status: 500 } ); } } ``` ```ts Signature Verification (app/api/auth/verify/route.ts) lines wrap expandable theme={null} import { NextRequest, NextResponse } from 'next/server'; import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; import { nonceStore } from '@/lib/nonce-store'; const client = createPublicClient({ chain: base, transport: http() }); export async function POST(request: NextRequest) { try { const { address, message, signature } = await request.json(); // Extract nonce from SIWE message const nonce = message.match(/Nonce: (\w+)/)?.[1]; if (!nonce || !nonceStore.consume(nonce)) { return NextResponse.json( { error: 'Invalid or reused nonce' }, { status: 400 } ); } // Verify signature using viem const valid = await client.verifyMessage({ address: address as `0x${string}`, message, signature: signature as `0x${string}` }); if (!valid) { return NextResponse.json( { error: 'Invalid signature' }, { status: 401 } ); } return NextResponse.json({ success: true, address, timestamp: new Date().toISOString() }); } catch (error) { return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } } ``` ```ts Nonce Store (lib/nonce-store.ts) lines wrap expandable theme={null} // Simple in-memory nonce store // In production, use Redis or a database class NonceStore { private nonces = new Set ### Production Considerations For production deployments, enhance the backend implementation with: * **Persistent storage**: Use Redis or a database instead of in-memory storage * **Rate limiting**: Implement request rate limiting for nonce generation * **Session management**: Create proper JWT tokens or session cookies * **Nonce expiration**: Add timestamp-based nonce expiration # Setup Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/privy/setup Configure Privy with Base Account for your React application Learn how to set up Privy with Base Account to enable seamless user authentication and wallet management. ## Overview [Privy](https://www.privy.io/) provides user authentication and wallet management solutions for onchain applications. By integrating Privy with Base Account, you can access all the Privy hooks and methods while having access to the users of Base Account. ### What You'll Achieve By the end of this guide, you will: * Set up Privy with Base Account support * Have Base Account set up as the main authentication option * Be able to access Base Account SDK from Privy's React SDK You can jump ahead and use the [Base Account Privy Template](https://github.com/base/base-account-privy) to get started.(); add(nonce: string): void { this.nonces.add(nonce); } consume(nonce: string): boolean { return this.nonces.delete(nonce); } } export const nonceStore = new NonceStore(); ``` ## Installation ### 1. Create a New Next.js Project ```bash npm theme={null} npx create-next-app@latest base-account-privy cd base-account-privy ``` ```bash yarn theme={null} yarn create next-app base-account-privy cd base-account-privy ``` ### 2. Override the Base Account SDK Version In order to access the latest version of the Base Account SDK, you need to override the Privy pinned version in your package.json file. To do this, you can use the following command to override it:```bash npm theme={null} npm pkg set overrides.@base-org/account="latest" # OR manually add to package.json: # "overrides": { "@base-org/account": "latest" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "latest" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "latest" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "latest" } ``` Or you can use a specific version by adding the version to the overrides:```bash npm theme={null} npm pkg set overrides.@base-org/account="2.2.0" # OR manually add to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "2.2.0" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "2.2.0" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` **If you're not starting a new projects** Make sure to delete your `node_modules` and `package-lock.json` and run a new install to ensure the overrides are applied. ### 3. Install the Dependencies Install the dependencies with your package manager of choice:```bash npm theme={null} npm install @privy-io/react-auth @privy-io/chains @privy-io/wagmi-connector wagmi viem @base-org/account-ui react-toastify ``` ```bash pnpm theme={null} pnpm add @privy-io/react-auth @privy-io/chains @privy-io/wagmi-connector wagmi viem @base-org/account-ui react-toastify ``` ```bash yarn theme={null} yarn add @privy-io/react-auth @privy-io/chains @privy-io/wagmi-connector wagmi viem @base-org/account-ui react-toastify ``` ```bash bun theme={null} bun add @privy-io/react-auth @privy-io/chains @privy-io/wagmi-connector wagmi viem @base-org/account-ui react-toastify ``` ## Configuration ### 1. Set Up Environment Variables Create a `.env.local` file in your project root: ```bash .env.local theme={null} NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id ``` Get your Privy App ID from the [Privy Dashboard](https://dashboard.privy.io/). ### 2. Configure Privy Provider Create your Privy configuration with Base Account as the default login method and update the layout to include the `PrivyProvider`.```tsx Create Provider (app/providers.tsx) lines wrap expandable theme={null} "use client"; import { PrivyProvider } from "@privy-io/react-auth"; import { base } from "@privy-io/chains"; export default function Providers({ children }: { children: React.ReactNode }) { return ( ## Usage ### 1. Update the App Page Update the `app/page.tsx` file to show the authentication flow: ```tsx app/page.tsx lines wrap expandable theme={null} "use client"; import { usePrivy } from "@privy-io/react-auth"; import { ToastContainer } from "react-toastify"; function Home() { const { ready, authenticated, logout, login } = usePrivy(); if (!ready) { return{children} ); } ``` ```tsx Add to Layout (app/layout.tsx) lines wrap expandable theme={null} import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import Providers from "./providers"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "Privy Next demo", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ({children} ); } ```Loading...; } return ({authenticated ? (); } export default Home; ``` ### 2. Run the Project Locally You're done! You can now run the project locally:) : ( )} Base × Privy DemoBuild on BaseGet started building on Base with Privy's authentication and native Base Account support```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash yarn theme={null} yarn dev ``` ```bash bun theme={null} bun dev ``` You should see a page that looks like this:### 3. Get the Base Account SDK Instance (Optional) You can access the Base Account SDK from Privy using the `useBaseAccount` hook. ```tsx Get the SDK instance lines wrap expandable theme={null} import { useBaseAccountSdk } from '@privy-io/react-auth'; const { baseAccountSdk } = useBaseAccountSdk(); const provider = baseAccountSdk.getProvider(); const addresses = await provider.request({method: 'wallet_connect'}); ``` # Spend Permissions Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/privy/spend-permissions Enable trusted spenders to move assets without additional signatures Learn how to create and manage Spend Permissions that allow trusted spenders to move assets from Base Accounts without requiring additional user signatures. ## Overview Spend Permissions enable users to grant timely allowances to trusted spenders, allowing them to move assets on behalf of the user within defined limits. This creates seamless user experiences for recurring payments, subscriptions, and automated transactions. ### What You'll Achieve By the end of this guide, you will: * Create Spend Permissions with specific allowances and time periods * Fetch and manage existing Spend Permissions * Use Spend Permissions to execute transactions * Implement Spend Permission functionality in your React application The code snippets in this guide are based on the following example project:![]()
## Setup Follow the [Setup](/sdks/base-account/framework-integrations/privy/setup) guide to set up Privy with Base Account. ## Implementation ### Component Setup ```tsx Spend Permissions Component (components/sections/spend-permissions.tsx) lines wrap expandable theme={null} "use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { useBaseAccountSdk, useWallets } from "@privy-io/react-auth"; import { requestSpendPermission, prepareSpendCallData, fetchPermissions, getPermissionStatus, type SpendPermission, } from "@base-org/account/spend-permission/browser"; import { base } from "@privy-io/chains"; export const SpendPermissions = () => { const { baseAccountSdk } = useBaseAccountSdk(); const { wallets } = useWallets(); const [permissions, setPermissions] = useState ### Key Methods #### Creating Spend Permissions Use `requestSpendPermission` to create new Spend Permissions: ```tsx Create a Spend Permission lines wrap expandable theme={null} const permission = await requestSpendPermission({ account, spender: spenderAddress, token: tokenAddress, chainId: base.id, allowance: BigInt(1) * BigInt(10 ** 6), // 1 USDC periodInDays: 1, // 1 day provider, }); ``` #### Fetching Permissions Use `fetchPermissions` to retrieve existing permissions: ```tsx Fetch Permissions theme={null} const permissions = await fetchPermissions({ account, chainId: base.id, spender: spenderAddress, provider, }); ``` #### Using Permissions Check permission status and prepare spend calls: ```tsx Use a Permission theme={null} // Check if permission is active const { isActive, remainingSpend } = await getPermissionStatus(permission); // Prepare spend transaction data const spendCalls = await prepareSpendCallData(permission, spendAmount); ``` ### Configuration Options #### Allowance and Period Configure spending limits and time periods: ```tsx Allowance and Period Options theme={null} { allowance: BigInt(100) * BigInt(10 ** 6), // 100 USDC (6 decimals) periodInDays: 7, // 7 days } ``` #### Token Addresses Common token addresses on Base: * **USDC**: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` * **ETH**: Native token `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` * **DAI**: `0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb` ### Use Cases Spend permissions are ideal for: * **Subscriptions**: Recurring payments without user interaction * **DeFi protocols**: Automated trading and yield farming * **Gaming**: In-game purchases and rewards * **Commerce**: Streamlined checkout experiences([]); const [selectedPermission, setSelectedPermission] = useState (null); const [loading, setLoading] = useState(false); // Configuration const spenderAddress = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; const tokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base // Find the Base Account wallet const baseAccount = useMemo(() => { return wallets.find((wallet) => wallet.walletClientType === 'base_account'); }, [wallets]); const provider = baseAccountSdk?.getProvider(); const account = baseAccount?.address; const loadPermissions = useCallback(async () => { if (!account || !provider || !spenderAddress) return; try { setLoading(true); const fetchedPermissions = await fetchPermissions({ account, chainId: base.id, spender: spenderAddress, provider, }); setPermissions(fetchedPermissions); } catch (error) { console.error("Failed to load permissions:", error); } finally { setLoading(false); } }, [account, provider, spenderAddress]); const handleRequestSpendPermission = async () => { if (!account || !provider || !spenderAddress || !tokenAddress) return; try { setLoading(true); const permission = await requestSpendPermission({ account, spender: spenderAddress, token: tokenAddress, chainId: base.id, allowance: BigInt(1) * BigInt(10 ** 6), // 1 USDC (6 decimals) periodInDays: 1, // 1 day provider, }); setPermissions([...permissions, permission]); } catch (error) { console.error("Failed to create Spend Permission:", error); } finally { setLoading(false); } }; const handleUseSpendPermission = async () => { if (!selectedPermission || !provider || !spenderAddress) return; try { setLoading(true); // Check permission status const { isActive, remainingSpend } = await getPermissionStatus(selectedPermission); if (!isActive) { console.error("Selected permission is not active"); return; } const spendAmount = BigInt(100) * BigInt(10 ** 6); // 100 USDC if (remainingSpend < spendAmount) { console.error("Insufficient remaining allowance"); return; } // Prepare spend calls const spendCalls = await prepareSpendCallData(selectedPermission, spendAmount); console.log("Spend calls prepared:", spendCalls); } catch (error) { console.error("Failed to use Spend Permission:", error); } finally { setLoading(false); } }; return ( ); }; ```{/* Configuration display */}{/* Permissions list */} {permissions.length > 0 && (Configuration
Spender: {spenderAddress}Token: {tokenAddress} (USDC)Allowance: $1 USDC per day)}Existing Permissions
{permissions.map((permission, index) => (setSelectedPermission(permission)} >))}Spender: {permission.permission.spender?.slice(0, 10)}...Token: {permission.permission.token?.slice(0, 10)}...Allowance: {permission.permission.allowance?.toString()}# Sub Accounts Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/privy/sub-accounts Create and manage app-specific wallet accounts with Base Account Learn how to create and manage Sub Accounts that provide app-specific wallet accounts embedded directly in your application. ## Overview Sub Accounts allow you to provision dedicated wallet accounts for your users within your application. These accounts are controlled by the user's main Base Account and provide an enhanced user experience as the user's interactions produce no passkey prompts or popups. Users can manage all their Sub Accounts at [account.base.app](https://account.base.app). If you would like to see a live demo of Sub Accounts in action, check out our [Sub Accounts Demo](https://sub-accounts-fc.vercel.app). ### What You'll Achieve By the end of this guide, you will: * Understand how Sub Accounts work with Base Account * Create new Sub Accounts for users * Retrieve and manage existing Sub Accounts * Implement Sub Account in your Privy project The code snippets in this guide are based on the following example project:## Setup Follow the [Setup](/sdks/base-account/framework-integrations/privy/setup) guide to set up Privy with Base Account. ## Implementation ### Component Setup ```tsx Sub Accounts Component (components/sections/sub-accounts.tsx) lines wrap expandable theme={null} "use client"; import { useState, useMemo } from "react"; import { useWallets } from "@privy-io/react-auth"; const SubAccounts = () => { const { wallets } = useWallets(); const [subAccounts, setSubAccounts] = useState< { address: string; factory: string; factoryData: string; }[] >([]); const [isLoading, setIsLoading] = useState(false); // Find the Base Account wallet const baseAccount = useMemo(() => { return wallets.find((wallet) => wallet.walletClientType === 'base_account'); }, [wallets]); const handleGetSubAccounts = async () => { if (!baseAccount) return; setIsLoading(true); try { // Switch to Base Sepolia (or Base Mainnet - use 8453 for mainnet) await baseAccount.switchChain(84532); const provider = await baseAccount.getEthereumProvider(); // Get existing Sub Accounts const response = await provider.request({ method: 'wallet_getSubAccounts', params: [{ account: baseAccount.address, domain: window.location.origin }] }); const { subAccounts: existingSubAccounts } = response; setSubAccounts(existingSubAccounts || []); } catch (error) { console.error("Error getting Sub Accounts:", error); } finally { setIsLoading(false); } }; const handleAddSubAccount = async () => { if (!baseAccount) return; setIsLoading(true); try { // Switch to Base Sepolia (or Base Mainnet - use 8453 for mainnet) await baseAccount.switchChain(84532); const provider = await baseAccount.getEthereumProvider(); // Create new Sub Account await provider.request({ method: 'wallet_addSubAccount', params: [{ version: '1', account: { type: 'create', keys: [{ type: 'address', publicKey: baseAccount.address }] } }] }); // Refresh the Sub Accounts list await handleGetSubAccounts(); } catch (error) { console.error("Error creating Sub Account:", error); } finally { setIsLoading(false); } }; return ( ### Key Methods #### Getting Sub Accounts Use `wallet_getSubAccounts` to retrieve existing Sub Accounts for a domain: ```tsx Get Sub Accounts (components/sections/sub-accounts.tsx) theme={null} const response = await provider.request({ method: 'wallet_getSubAccounts', params: [{ account: baseAccount.address, domain: window.location.origin }] }); ``` #### Creating Sub Accounts Use `wallet_addSubAccount` to create new Sub Accounts: ```tsx Create Sub Account (components/sections/sub-accounts.tsx) lines wrap expandable theme={null} await provider.request({ method: 'wallet_addSubAccount', params: [{ version: '1', account: { type: 'create', keys: [{ type: 'address', publicKey: baseAccount.address }] } }] }); ``` ### Network Configuration Sub accounts work on both Base Mainnet and Base Sepolia: * **Base Mainnet**: Chain ID `8453` * **Base Sepolia**: Chain ID `84532` ### Explore Further * [Sub Accounts Guide](/sdks/base-account/improve-ux/sub-accounts) * [Privy Sub Accounts Recipe](https://docs.privy.io/recipes/react/external-wallets/base-sub-accounts) # Wallet Actions Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/privy/wallet-actions Sign messages, transactions, and typed data with Privy wallets Learn how to perform wallet actions including signing messages, typed data, and transactions for EVM wallets using Privy. ## Overview Privy provides comprehensive [wallet action hooks](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction) that work seamlessly with EVM (Ethereum-compatible) wallets. You can sign messages, typed data, raw hashes, and transactions, as well as send transactions directly. ### What You'll Achieve By the end of this guide, you will: * Sign messages for EVM wallets * Sign typed data (EIP-712) for structured data * Send transactions on EVM networks The code snippets in this guide are based on the following example project:); }; ```{subAccounts.length > 0 && ()}Existing Sub Accounts:
{subAccounts.map((subAccount, index) => ())}Address: {subAccount.address}
Factory: {subAccount.factory}
Factory Data: {subAccount.factoryData}
## Implementation ### Component Setup ```tsx Wallet Actions Component (components/sections/wallet-actions.tsx) lines wrap expandable theme={null} "use client"; import { useState, useMemo, useEffect } from "react"; import { useWallets, useSendTransaction, useSignMessage, useSignTypedData, } from "@privy-io/react-auth"; const WalletActions = () => { const { signMessage } = useSignMessage(); const { sendTransaction } = useSendTransaction(); const { signTypedData } = useSignTypedData(); const { wallets } = useWallets(); const [selectedWallet, setSelectedWallet] = useState<{ address: string; type: string; name: string; } | null>(null); // Map wallets for selection const allWallets = useMemo(() => { return wallets.map((wallet) => ({ address: wallet.address, type: "ethereum", name: wallet.address, })); }, [wallets]); useEffect(() => { if (allWallets.length > 0 && !selectedWallet) { setSelectedWallet(allWallets[0]); } }, [allWallets, selectedWallet]); const handleSignMessage = async () => { if (!selectedWallet) return; try { const message = "Hello, world!"; const { signature } = await signMessage( { message }, { address: selectedWallet.address } ); console.log("Message signed:", signature); } catch (error) { console.error("Failed to sign message:", error); } }; const handleSendTransaction = async () => { if (!selectedWallet) return; try { const transaction = await sendTransaction( { to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: 10000 }, { address: selectedWallet.address } ); console.log("Transaction sent:", transaction); } catch (error) { console.error("Failed to send transaction:", error); } }; const handleSignTypedData = async () => { if (!selectedWallet) return; try { const typedData = { domain: { name: "Example App", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", }, types: { Person: [ { name: "name", type: "string" }, { name: "wallet", type: "address" }, ], Mail: [ { name: "from", type: "Person" }, { name: "to", type: "Person" }, { name: "contents", type: "string" }, ], }, primaryType: "Mail", message: { from: { name: "Alice", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", }, to: { name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", }, contents: "Hello, Bob!", }, }; const { signature } = await signTypedData(typedData, { address: selectedWallet.address, }); console.log("Typed data signature:", signature); } catch (error) { console.error("Failed to sign typed data:", error); } }; return ( ### Wallet Actions #### Sign Message{/* Wallet Selection */}); }; ```{/* Action Buttons */}```tsx Sign Message lines wrap expandable theme={null} const handleSignMessage = async () => { if (!selectedWallet) return; try { const message = "Hello, world!"; const { signature } = await signMessage( { message }, { address: selectedWallet.address } ); console.log("Signature:", signature); } catch (error) { console.error("Failed to sign message:", error); } }; ``` #### Sign Typed Data (EIP-712)```tsx Sign Typed Data lines wrap expandable theme={null} const handleSignTypedData = async () => { if (!selectedWallet) return; try { const typedData = { domain: { name: "Example App", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", }, types: { Person: [ { name: "name", type: "string" }, { name: "wallet", type: "address" }, ], Mail: [ { name: "from", type: "Person" }, { name: "to", type: "Person" }, { name: "contents", type: "string" }, ], }, primaryType: "Mail", message: { from: { name: "Alice", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", }, to: { name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", }, contents: "Hello, Bob!", }, }; const { signature } = await signTypedData(typedData, { address: selectedWallet.address, }); console.log("Typed data signature:", signature); } catch (error) { console.error("Failed to sign typed data:", error); } }; ``` #### Send Transaction```tsx Send Transaction lines wrap expandable theme={null} const handleSendTransaction = async () => { if (!selectedWallet) return; try { const transaction = await sendTransaction( { to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: 10000 // Wei }, { address: selectedWallet.address } ); console.log("Transaction hash:", transaction); } catch (error) { console.error("Failed to send transaction:", error); } }; ``` ## Explore Further * [Privy docs](https://docs.privy.io/) * [Batch Transactions](/sdks/base-account/improve-ux/batch-transactions) * [Sponsor Gas](/sdks/base-account/improve-ux/sponsor-gas/paymasters) # RainbowKit Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/rainbowkit Integrate Base Account with RainbowKit ## Overview [RainbowKit](https://www.rainbowkit.com/) is a React library that makes it easy to add wallet sign-in to your onchain application. It's designed to work out-of-the-box and includes native support for Base Account. By integrating RainbowKit with Base Account, you can provide your users with a seamless onboarding experience while maintaining access to the full Base Account feature set. ### What You'll Achieve By the end of this guide, you will: * Set up RainbowKit with Base Account support * Learn how to use both `ConnectButton` and `WalletButton` components * Configure your app to prioritize Base Account as the primary wallet option * Obtain and configure a Reown project ID (required for RainbowKit projects) You can jump ahead and use the Base Account RainbowKit Template to get started:## Installation After [creating a new Next.js project](https://nextjs.org/docs/app/getting-started/installation), install the required dependencies: ```bash npm theme={null} npm install @rainbow-me/rainbowkit wagmi viem @tanstack/react-query ``` ```bash pnpm theme={null} pnpm add @rainbow-me/rainbowkit wagmi viem @tanstack/react-query ``` ```bash yarn theme={null} yarn add @rainbow-me/rainbowkit wagmi viem @tanstack/react-query ``` ```bash bun theme={null} bun add @rainbow-me/rainbowkit wagmi viem @tanstack/react-query ``` **Access the latest version of the Base Account SDK (Recommended)** It is to access the latest version of the Base Account SDK in order to get the latest features and bug fixes. To do this, you can use the following command to override it: ## Get Your Reown Project ID Before you can use RainbowKit with Base Account, you need to obtain a project ID from Reown Cloud. 1. Visit [Reown Cloud Dashboard](https://dashboard.reown.com/) 2. Sign up for a free account or log in if you already have one 3. Create a new project and copy the project ID. ## Configuration ### 1. Configure wagmi with RainbowKit Create a `wagmi.ts` file in your `src` directory to configure your blockchain connections and wallet options: ```tsx src/wagmi.ts lines wrap expandable theme={null} import { getDefaultConfig } from '@rainbow-me/rainbowkit'; import { base, mainnet } from 'wagmi/chains'; export const config = getDefaultConfig({ appName: 'My Base Account App', projectId: 'YOUR_PROJECT_ID', // Replace with your Reown project ID chains: [ mainnet, base ], ssr: true, // Enable server-side rendering support }); ``````bash npm theme={null} npm pkg set overrides.@base-org/account="latest" # OR manually add to package.json: # "overrides": { "@base-org/account": "latest" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "latest" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "latest" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "latest" } ``` Or you can use a specific version by adding the version to the overrides:```bash npm theme={null} npm pkg set overrides.@base-org/account="2.2.0" # OR manually add to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "2.2.0" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "2.2.0" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` Make sure to delete your `node_modules` and `package-lock.json` and run a new install to ensure the overrides are applied.**Replace YOUR\_PROJECT\_ID** Make sure to replace `'YOUR_PROJECT_ID'` with the actual project ID you obtained from [Reown Cloud](https://dashboard.reown.com/). For production applications, use environment variables: ```typescript theme={null} projectId: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID!, ``` And add to your `.env.local`: ```bash theme={null} NEXT_PUBLIC_REOWN_PROJECT_ID=your_project_id_here ``` ### 2. Set Up RainbowKit Provider Wrap your application with the necessary providers in your `_app.tsx`: ```tsx src/pages/_app.tsx lines wrap expandable theme={null} import '../styles/global.css'; import '@rainbow-me/rainbowkit/styles.css'; import type { AppProps } from 'next/app'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WagmiProvider } from 'wagmi'; import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; import { config } from '../wagmi'; const queryClient = new QueryClient(); function MyApp({ Component, pageProps }: AppProps) { return (); } export default MyApp; ``` ## Usage RainbowKit provides two main components for wallet connections: `ConnectButton` and `WalletButton`. Both components support Base Account out of the box. ### Option 1: Using ConnectButton The `ConnectButton` is RainbowKit's all-in-one wallet connection component. It displays the wallet connection modal with all available wallets, including Base Account. ```tsx src/pages/index.tsx lines wrap expandable theme={null} import { ConnectButton } from '@rainbow-me/rainbowkit'; import type { NextPage } from 'next'; const Home: NextPage = () => { return ( ); }; export default Home; ``` When implemented, this is what it will look like: ### Option 2: Using WalletButton for Base Account The `WalletButton` component provides a direct connection to a specific wallet. This is ideal when you want to highlight Base Account as the primary wallet option. ```tsx src/pages/index.tsx lines wrap expandable theme={null} import { WalletButton } from '@rainbow-me/rainbowkit'; import type { NextPage } from 'next'; const Home: NextPage = () => { return (
); }; export default Home; ``` When implemented, this is what it will look like: ## Advanced Configuration ### Prioritize Base Account in Wallet List To make Base Account appear first in the wallet connection modal, you can customize the wallet order: ```tsx src/wagmi.ts lines wrap expandable theme={null} import { getDefaultConfig } from '@rainbow-me/rainbowkit'; import { base, mainnet, sepolia } from 'wagmi/chains'; export const config = getDefaultConfig({ appName: 'My Base Account App', projectId: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID!, chains: [base, mainnet, sepolia], ssr: true, // Wallet configuration wallets: [ { groupName: 'Recommended', wallets: ['baseAccount'], // Base Account appears first }, ], }); ``` ### Customize RainbowKit Theme RainbowKit supports extensive theming options: ```tsx src/pages/_app.tsx lines wrap expandable theme={null} import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit'; function MyApp({ Component, pageProps }: AppProps) { return (![]()
); } ``` ### Access Wallet Connection State Use wagmi hooks to access wallet connection state throughout your app: ```tsx Profile.tsx lines wrap expandable theme={null} import { useAccount, useDisconnect, useEnsName } from 'wagmi'; function Profile() { const { address, isConnected } = useAccount(); const { disconnect } = useDisconnect(); const { data: ensName } = useEnsName({ address }); if (!isConnected) return Not connected; return (); } ``` ### Switch Networks Programmatically Allow users to switch between different chains: ```tsx NetworkSwitcher.tsx lines wrap expandable theme={null} import { useSwitchChain } from 'wagmi'; import { base, mainnet } from 'wagmi/chains'; function NetworkSwitcher() { const { switchChain } = useSwitchChain(); return (Connected to {ensName ?? address}
); } ``` ## Best PracticesStore sensitive configuration like your Reown project ID in environment variables, not in your code: ```bash .env.local theme={null} NEXT_PUBLIC_REOWN_PROJECT_ID=your_project_id_here ``` Always set `ssr: true` in your wagmi config for Next.js applications to avoid hydration issues: ```typescript theme={null} export const config = getDefaultConfig({ // ... ssr: true, }); ``` Put Base as the first chain in your configuration to make it the default: ```typescript theme={null} chains: [base, mainnet, ...otherChains] ``` Regularly update RainbowKit, wagmi, and viem to get the latest Base Account features and security patches: ```bash theme={null} npm update @rainbow-me/rainbowkit wagmi viem ``` ## Next Steps Now that you have RainbowKit configured with Base Account, you can:# Reown Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/reown Integrate Base Account with Reown AppKit for your React application ## Overview [Reown AppKit](https://reown.com/appkit) (formerly WalletConnect) is a library for adding wallet connections to your onchain applications. It provides a polished modal interface with support for multiple wallets, email authentication, and social logins. By integrating Reown with Base Account, you can provide users with seamless onboarding through Base's native smart wallet while maintaining access to other sign in methods. ### What You'll Achieve By the end of this guide, you will: * Set up Reown AppKit with Base Account as a featured wallet option * Configure the modal to prioritize Base Account in the wallet list * Optionally add a custom Coinbase Wallet connector for legacy SDK access You can jump ahead and use the Base Account Reown Template to get started: Learn more about Base Account and its features Learn more about RainbowKit and its features Learn more about wagmi and its features Join the Base community and get help from other developers ## Installation After [creating a new Next.js project](https://nextjs.org/docs/app/getting-started/installation), install the required dependencies: ```bash npm theme={null} npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` ```bash yarn theme={null} yarn add @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` ```bash pnpm theme={null} pnpm add @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` ```bash bun theme={null} bun add @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query ``` ## Get Your Reown Project ID Before you can use Reown AppKit, you need to obtain a project ID from Reown Cloud. 1. Visit [Reown Cloud Dashboard](https://dashboard.walletconnect.com/) 2. Sign up for a free account or log in if you already have one 3. Create a new project and copy the project ID ## Configuration ### 1. Set Up Environment Variables Create a `.env.local` file in your project root: ```bash .env.local theme={null} NEXT_PUBLIC_PROJECT_ID=your_project_id_here ``` ### 2. Configure the wagmi Adapter Create a config file to set up the Wagmi adapter with your networks: ```typescript src/config/index.ts lines wrap expandable theme={null} import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' import { arbitrum, base } from '@reown/appkit/networks' import type { AppKitNetwork } from '@reown/appkit/networks' export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID! if (!projectId) { throw new Error('Project ID is not defined') } export const networks = [base, arbitrum] as [AppKitNetwork, ...AppKitNetwork[]] export const wagmiAdapter = new WagmiAdapter({ ssr: true, projectId, networks }) export const config = wagmiAdapter.wagmiConfig ``` ### 3. Create the AppKit Provider Create a context provider that initializes AppKit and wraps your application: ```tsx src/context/index.tsx lines wrap expandable theme={null} 'use client' import { wagmiAdapter, projectId, networks } from '@/config' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createAppKit } from '@reown/appkit/react' import React, { type ReactNode } from 'react' import { cookieToInitialState, WagmiProvider, type Config } from 'wagmi' const queryClient = new QueryClient() const metadata = { name: 'My Base App', description: 'My Base Account App', url: 'https://myapp.com', icons: ['https://myapp.com/icon.png'] } // Base Account wallet ID const BASE_ACCOUNT_WALLET_ID = 'fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa' export const modal = createAppKit({ adapters: [wagmiAdapter], projectId, networks, metadata, themeMode: 'light', features: { analytics: true, connectMethodsOrder: ['wallet', 'email', 'social'], // Wallets appear first }, themeVariables: { '--w3m-accent': '#0052FF', // Base blue }, // Prioritize Base Account wallet featuredWalletIds: [BASE_ACCOUNT_WALLET_ID], includeWalletIds: [BASE_ACCOUNT_WALLET_ID], allWallets: 'SHOW', enableWallets: true, }) function ContextProvider({ children, cookies }: { children: ReactNode; cookies: string | null }) { const initialState = cookieToInitialState(wagmiAdapter.wagmiConfig as Config, cookies) return () } export default ContextProvider ``` ### 4. Add Provider to Layout Update your root layout to use the context provider: ```tsx src/app/layout.tsx lines wrap expandable theme={null} import type { Metadata } from 'next' import { headers } from 'next/headers' import './globals.css' import ContextProvider from '@/context' export const metadata: Metadata = { title: 'Base Account Reown App', description: 'Base Account with Reown AppKit', } export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { const headersData = await headers() const cookies = headersData.get('cookie') return ( {children} {children} ) } ```To learn more about configuration options for Reown AppKit, please refer to the [Reown AppKit documentation](https://docs.reown.com/appkit). ## Usage ### Using the AppKit Button Reown provides a web component for the connect button. Create a component to use it: ```tsx src/components/ConnectButton.tsx theme={null} 'use client' export default function ConnectButton() { return} ``` Then use it in your page: ```tsx src/app/page.tsx lines wrap expandable theme={null} import ConnectButton from '@/components/ConnectButton' export default function Home() { return ( ) } ``` ### Run the Project Locally ```bash npm theme={null} npm run dev ``` ```bash yarn theme={null} yarn dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun dev ``` You should see a page with a connect button. Clicking it will open the Reown modal with Base Account as the featured wallet option.## Adding Coinbase Wallet SDK Connector If you need access to the legacy Coinbase Wallet SDK (for EOA wallet support), you can add a custom connector alongside Base Account.![]()
Base Account uses the newer Base Account SDK. If your application specifically requires the legacy Coinbase Wallet SDK features, you can add a custom connector as shown below. ### Update the wagmi Adapter Modify your config to include the Coinbase Wallet connector: ```typescript src/config/index.ts lines wrap expandable theme={null} import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' import { arbitrum, base } from '@reown/appkit/networks' import type { AppKitNetwork } from '@reown/appkit/networks' import { coinbaseWallet } from 'wagmi/connectors' export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID! if (!projectId) { throw new Error('Project ID is not defined') } export const networks = [base, arbitrum] as [AppKitNetwork, ...AppKitNetwork[]] // Add custom Coinbase Wallet connector for legacy SDK access const connectors = [ coinbaseWallet({ appName: 'My Base App', appLogoUrl: 'https://myapp.com/logo.png', preference: 'all', // 'all' | 'smartWalletOnly' | 'eoaOnly' }), ] export const wagmiAdapter = new WagmiAdapter({ ssr: true, projectId, networks, connectors, // Add custom connectors }) export const config = wagmiAdapter.wagmiConfig ``` ### Coinbase Wallet Preference Options The `preference` option controls which wallet type is displayed: | Option | Description | | ----------------- | ---------------------------------------------------------- | | `all` | Shows both Smart Wallet and EOA options (default) | | `smartWalletOnly` | Only shows Coinbase Smart Wallet popup | | `eoaOnly` | Only shows EOA Browser Extension or Mobile Coinbase Wallet | # thirdweb Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/thirdweb Configure Thirdweb with Base Account for your React application Learn how to set up Thirdweb with Base Account to enable seamless user authentication and wallet management. ## Overview [Thirdweb](https://thirdweb.com/) provides a complete onchain application development framework with wallet connection, authentication, and smart contract interactions. By integrating Thirdweb with Base Account, you can leverage Thirdweb's `ConnectButton` component while providing users with native Base Account wallet support. ### What You'll Achieve By the end of this guide, you will: * Set up Thirdweb with Base Account support * Have Base Account available as a wallet option alongside email authentication * Use Thirdweb's `ConnectButton` for a polished wallet connection experience You can jump ahead and use the [Base Account Thirdweb Template](https://github.com/base/demos/tree/main/base-account/base-account-thirdweb-template) to get started.## Installation ### 1. Create a New Next.js Project ```bash npm theme={null} npx create-next-app@latest base-account-thirdweb cd base-account-thirdweb ``` ```bash yarn theme={null} yarn create next-app base-account-thirdweb cd base-account-thirdweb ``` ```bash pnpm theme={null} pnpm create next-app base-account-thirdweb cd base-account-thirdweb ``` ```bash bun theme={null} bun create next-app base-account-thirdweb cd base-account-thirdweb ``` ### 2. Install the Dependencies Install the Thirdweb SDK (version 5.118.0 or higher is required for Base Account support):```bash npm theme={null} npm install thirdweb@^5.118.0 ``` ```bash yarn theme={null} yarn add thirdweb@^5.118.0 ``` ```bash pnpm theme={null} pnpm add thirdweb@^5.118.0 ``` ```bash bun theme={null} bun add thirdweb@^5.118.0 ``` ## Configuration ### 1. Set Up Environment Variables Create a `.env.local` file in your project root: ```bash .env.local theme={null} NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your-client-id ``` Get your Client ID from the [Thirdweb Dashboard](https://thirdweb.com/dashboard). ### 2. Create the thirdweb Client Create a client configuration file: ```ts src/lib/client.ts theme={null} import { createThirdwebClient } from "thirdweb"; export const client = createThirdwebClient({ clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!, }); ``` ### 3. Configure ThirdwebProvider Create a providers wrapper component and update your layout:```tsx Create Provider (src/providers/providers.tsx) expandable theme={null} "use client"; import { ThirdwebProvider } from "thirdweb/react"; export default function Providers({ children }: { children: React.ReactNode }) { return ## Usage Use Thirdweb's `ConnectButton` with your wallet configuration: ```tsx src/app/page.tsx lines wrap expandable theme={null} "use client"; import dynamic from "next/dynamic"; import { lightTheme } from "thirdweb/react"; import { inAppWallet, createWallet } from "thirdweb/wallets"; import { base } from "thirdweb/chains"; import { client } from "@/lib/client"; // Dynamic import to avoid SSR hydration issues const ConnectButton = dynamic( () => import("thirdweb/react").then((mod) => mod.ConnectButton), { ssr: false } ); // Configure wallets const emailWallet = inAppWallet({ auth: { options: ["email"], }, }); const baseAccountWallet = createWallet("org.base.account"); const wallets = [emailWallet, baseAccountWallet]; const recommendedWallets = [baseAccountWallet]; // Custom theme (optional) const customTheme = lightTheme({ colors: { primaryButtonBg: "#0052FF", primaryButtonText: "#FFFFFF", accentText: "#0052FF", }, }); export default function Home() { return ({children} ; } ``` ```tsx Add to Layout (src/app/layout.tsx) lines wrap expandable theme={null} import type { Metadata } from "next"; import "./globals.css"; import Providers from "@/providers/providers"; export const metadata: Metadata = { title: "Base Account Thirdweb Template", description: "Build on Base with Thirdweb's authentication and Base Account support", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ({children} ); } ```); } ``` ### 3. Run the Project Locally You're done! Run the project locally: ```bash npm theme={null} npm run dev ``` ```bash yarn theme={null} yarn dev ``` ```bash pnpm theme={null} pnpm dev ``` ```bash bun theme={null} bun dev ``` You should see a page with a "Sign In" button. Clicking it will open the Thirdweb connect modal with Base Account and email authentication options.## Customization ### Wallet Options You can customize which wallets appear in the connect modal: ```tsx Wallet Options lines wrap expandable theme={null} // Email only const wallets = [inAppWallet({ auth: { options: ["email"] } })]; // Base Account only const wallets = [createWallet("org.base.account")]; // Multiple options including social logins const wallets = [ inAppWallet({ auth: { options: ["email", "google", "apple", "discord"], }, }), createWallet("org.base.account"), ]; ``` ### Theme Customize the appearance using `lightTheme` or `darkTheme`: ```tsx Theme Options lines wrap expandable theme={null} import { lightTheme, darkTheme } from "thirdweb/react"; // Light theme with custom colors const theme = lightTheme({ colors: { primaryButtonBg: "#0052FF", accentText: "#0052FF", modalBg: "#FFFFFF", }, }); // Or use dark theme const theme = darkTheme(); ``` ### Chain Change the default chain by importing from `thirdweb/chains`: ```tsx Chain Configuration theme={null} import { base, baseSepolia, mainnet } from "thirdweb/chains";![]()
``` ## Resources * [Thirdweb Documentation](https://portal.thirdweb.com) * [Thirdweb ConnectButton Reference](https://portal.thirdweb.com/references/typescript/v5/ConnectButton) * [Next.js Documentation](https://nextjs.org/docs) * [Base Support](https://discord.com/invite/buildonbase) # Base Pay Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/base-pay Accept USDC payments with Base Pay in your Wagmi-powered React application Base Pay works the same way in Wagmi applications as it does anywhere else - it operates independently of wallet connections and uses the Base Account SDK directly. ## Implementation Base Pay doesn't require any special Wagmi integration. Simply follow the [Accept Payments guide](/build-on-base/accept-payments/request-a-payment) - all the code examples work exactly the same in your Wagmi app. The key points: * **No wallet connection needed** - Base Pay handles everything through the SDK * **Same API** - Use `pay()` and `getPaymentStatus()` exactly as shown in the main guide * **Works alongside Wagmi** - You can display the user's connected address from `useAccount()` but it's not required for payments ## Quick Example ```tsx CheckoutButton.tsx lines wrap expandable theme={null} import { pay } from '@base-org/account' import { useAccount } from 'wagmi' // Optional - just for display export function CheckoutButton() { const { address } = useAccount() // Optional const handlePayment = async () => { try { const payment = await pay({ amount: '5.00', to: '0xYourAddress', testnet: true }) console.log('Payment sent:', payment.id) } catch (error) { console.error('Payment failed:', error) } } return ( {address &&) } ```Connected: {address}
}**Please Follow the Brand Guidelines** If you intend on using the `BasePayButton`, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. ## Learn More For complete implementation details, examples, and advanced features like collecting user information, see the main [Accept Payments guide](/build-on-base/accept-payments/request-a-payment). # Basenames Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/basenames Add support for Base names in your application using Wagmi and Viem ## Overview Basenames are human-readable names for addresses on Base. They are built on top of the ENS protocol and comply with [ENSIP-19](https://docs.ens.domains/ensip/19/). To learn more about Basenames, check out the [Basenames FAQ](/sdks/base-account/basenames/basenames-faq). This guide will show you how to add support for Basenames to your application using [Viem](https://viem.sh/). ## Usage Use `getEnsName` to retrieve the primary ENS name for an address on Base: ```ts getBasename.ts lines wrap expandable theme={null} import { createPublicClient, http, toCoinType } from 'viem' import { base } from 'viem/chains' const client = createPublicClient({ chain: mainnet, transport: http(YOUR_PRIVATE_RPC_URL), }) const name = await client.getEnsName({ address: '0x179A862703a4adfb29896552DF9e307980D19285', coinType: toCoinType(base.id), }) ```It is necessary to use a private RPC provider (`YOUR_PRIVATE_RPC_URL`) due to the computational demands associated with some of the ENSIP-19 resolution steps. There may be some latency between the initial registration of a Basename and the ability to resolve this name via ENSIP-19 due to the slow production of state proofs necessary for trustless resolution. [Learn more about getEnsName →](https://viem.sh/docs/ens/actions/getEnsName) # Batch Transactions Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/batch-transactions Send multiple onchain calls in a single transaction with Wagmi and Base Account Learn how to send multiple onchain calls in a single transaction with Wagmi and Base Account. ## Overview [Wagmi](https://wagmi.sh/) is a collection of React hooks for Ethereum Virtual Machine (EVM) compatible networks that makes it easy to work with wallets, contracts, transactions, and signing. Base Account integrates perfectly with Wagmi, allowing you to use all your familiar hooks. You can jump ahead and use the [Base Account Wagmi Template](https://github.com/base/demos/tree/master/base-account/base-account-wagmi-template) to get started.## Setup Make sure you have [set up Wagmi with Base Account](/sdks/base-account/framework-integrations/wagmi/setup) before following this guide. ## Basic Batch Transaction Send multiple ETH transfers in a single transaction by creating a component that uses the `sendCalls` method and adding a button to trigger the transaction. ```tsx components/BatchTransactions.tsx lines wrap expandable theme={null} "use client"; import { useState } from "react"; import { useSendCalls } from "wagmi"; import { encodeFunctionData, parseUnits } from "viem"; import { baseSepolia } from "wagmi/chains"; // USDC contract address on Base Sepolia const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // ERC20 ABI for the transfer function const erc20Abi = [ { inputs: [ { name: "to", type: "address" }, { name: "amount", type: "uint256" }, ], name: "transfer", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function", }, ] as const; export function BatchTransactions() { const { sendCalls, data, isPending, isSuccess, error } = useSendCalls(); const [amount1, setAmount1] = useState("1"); const [amount2, setAmount2] = useState("1"); const [usePaymaster, setUsePaymaster] = useState(false); async function handleBatchTransfer() { try { // Encode the first transfer call const call1Data = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [ "0x2211d1D0020DAEA8039E46Cf1367962070d77DA9", parseUnits(amount1, 6), // USDC has 6 decimals ], }); // Encode the second transfer call const call2Data = encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [ "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", parseUnits(amount2, 6), // USDC has 6 decimals ], }); // Prepare capabilities object if paymaster is enabled const capabilities = usePaymaster ? { paymasterService: { url: process.env.NEXT_PUBLIC_PAYMASTER_URL || "https://api.developer.coinbase.com/rpc/v1/base-sepolia", }, } : undefined; // Send the batch of calls sendCalls({ calls: [ { to: USDC_ADDRESS, data: call1Data, }, { to: USDC_ADDRESS, data: call2Data, }, ], chainId: baseSepolia.id, capabilities, }); } catch (err) { console.error("Error batching transactions:", err); } } return ( ); } ``` ```tsx app/page.tsx lines wrap expandable theme={null} "use client"; import { useAccount, useConnect, useDisconnect } from "wagmi"; import { SignInWithBase } from "../components/SignInWithBase"; import { BatchTransactions } from "../components/BatchTransactions"; function App() { const account = useAccount(); const { connectors, connect, status, error } = useConnect(); const { disconnect } = useDisconnect(); return ( <>Batch USDC Transfers
{isPending &&setAmount1(e.target.value)} placeholder="1" step="0.000001" min="0" />setAmount2(e.target.value)} placeholder="1" step="0.000001" min="0" />Transaction pending...} {isSuccess && data && ()} {error &&Batch sent successfully!
Batch ID: {data.id}
Error: {error.message}}Account
status: {account.status}{account.status === "connected" && ( )}
addresses: {JSON.stringify(account.addresses)}
chainId: {account.chainId}{account.status === "connected" &&Connect
{connectors.map((connector) => { if (connector.name === "Base Account") { return (); } else { return ( ); } })} {status}{error?.message}} > ); } export default App; ``` **You don't need to "Connect the wallet" first** Base Account allows you to prompt the user for sending the transaction using the `sendCalls` method without needing to "Connect the wallet" (ie. using `eth_requestAccounts`) first. # Other Use Cases Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/other-use-cases Access the Base Account provider from Wagmi for advanced functionality like Sub Accounts, Spend Permissions, and more Learn how to access the Base Account provider through Wagmi to unlock advanced Base Account features beyond basic authentication and payments. ## Prerequisites Make sure you have [set up Wagmi with Base Account](/sdks/base-account/framework-integrations/wagmi/setup) before following this guide. ## Getting the Provider The key to accessing advanced Base Account functionality is getting the provider from your Wagmi connector. Once you have the provider, you can use any Base Account RPC method.```tsx Hook lines wrap expandable theme={null} // hooks/useBaseAccountProvider.ts import { useConnections } from 'wagmi' import { useEffect, useState } from 'react' import { EIP1193Provider } from 'viem' export function useBaseAccountProvider() { const connections = useConnections() const [provider, setProvider] = useState ## Available Use Cases Once you have the provider, you can access all Base Account functionality: ### Sub Accounts Create and manage child accounts for improved UX. **Learn more:** [Sub Accounts Guide](/sdks/base-account/improve-ux/sub-accounts) | [Sub Accounts RPC Method](/sdks/base-account/reference/core/provider-rpc-methods/wallet_addSubAccount) ### Spend Permissions Allow apps to spend on behalf of users with predefined limits. **Learn more:** [Spend Permissions Guide](/sdks/base-account/improve-ux/spend-permissions) | [Spend Permissions Reference](/sdks/base-account/reference/spend-permission-utilities/requestSpendPermission) ### Batch Transactions Execute multiple transactions in a single user confirmation. **Learn more:** [Batch Transactions Guide](/sdks/base-account/improve-ux/batch-transactions) | [`wallet_sendCalls` Reference](/sdks/base-account/reference/core/provider-rpc-methods/wallet_sendCalls) ### Gasless Transactions Sponsor gas fees for your users. **Learn more:** [Gasless Transactions Guide](/sdks/base-account/improve-ux/sponsor-gas/paymasters) | [Coinbase Developer Platform Paymaster](https://docs.cdp.coinbase.com/paymaster/introduction/welcome) ### Full List of Provider Methods and Capabilities Access the full list of Base Account provider methods and capabilities. **Learn more:** [Provider RPC Methods](/sdks/base-account/reference/core/provider-rpc-methods/request-overview) | [Capabilities](/sdks/base-account/reference/core/capabilities/overview) # Setup Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/setup Configure Wagmi with Base Account connector for your React application Learn how to set up Wagmi with Base Account to enable Base Account SDK functionality with familiar React hooks. ## Overview [Wagmi](https://wagmi.sh/) is a collection of React hooks for Ethereum Virtual Machine (EVM) compatible networks that makes it easy to work with wallets, contracts, transactions, and signing. Base Account integrates perfectly with Wagmi, allowing you to use all your familiar hooks. You can jump ahead and use the [Base Account Wagmi Template](https://github.com/base/demos/tree/master/base-account/base-account-wagmi-template) to get started.(null) useEffect(() => { const connection = connections[0] if (!connection) { setProvider(null) return } connection.connector.getProvider().then((provider) => { setProvider(provider as EIP1193Provider) }) }, [connections]) return provider } ``` ```tsx Component lines wrap expandable theme={null} // components/BaseAccountFeatures.tsx import { useBaseAccountProvider } from '../hooks/useBaseAccountProvider' import { useAccount } from 'wagmi' export function BaseAccountFeatures() { const { address, isConnected } = useAccount() const provider = useBaseAccountProvider() const callProviderMethod = async (method: string, params: any[]) => { if (!provider) { console.error('Provider not available') return } try { const result = await provider.request({ method, params }) console.log(`${method} result:`, result) return result } catch (error) { console.error(`${method} error:`, error) throw error } } if (!isConnected) { return Please connect your wallet to access Base Account features
} return () } ```Base Account Features
Connected with Base Account provider. You can now access advanced features.
## Installation ### Option 1: New wagmi Project To create a new wagmi project, you can use the command line: ```bash npm theme={null} npm create wagmi@latest ``` ```bash pnpm theme={null} pnpm create wagmi ``` ```bash yarn theme={null} yarn create wagmi ``` ```bash bun theme={null} bun create wagmi ``` To get access to the latest version of the Base Account SDK within Wagmi, you can use the following command to override it: ```bash npm theme={null} npm pkg set overrides.@base-org/account="latest" # OR manually add to package.json: # "overrides": { "@base-org/account": "latest" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "latest" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "latest" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "latest" } ``` Or you can use a specific version by adding the version to the overrides:```bash npm theme={null} npm pkg set overrides.@base-org/account="2.2.0" # OR manually add to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "2.2.0" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "2.2.0" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` Install the dependencies with your package manager of choice: ```bash npm theme={null} npm install ``` ```bash pnpm theme={null} pnpm install ``` ```bash yarn theme={null} yarn install ``` ```bash bun theme={null} bun install ``` **If this is not your first install** Make sure to delete your `node_modules` and `package-lock.json` and run a new install to ensure the overrides are applied. ### Option 2: Existing ProjectTo get access to the latest version of the Base Account SDK within Wagmi, you can use the following command to override it: ```bash npm theme={null} npm pkg set overrides.@base-org/account="latest" # OR manually add to package.json: # "overrides": { "@base-org/account": "latest" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "latest" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "latest" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "latest" } ``` Or you can use a specific version by adding the version to the overrides:```bash npm theme={null} npm pkg set overrides.@base-org/account="2.2.0" # OR manually add to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` ```bash pnpm theme={null} # pnpm requires manual addition to package.json: # "pnpm": { "overrides": { "@base-org/account": "2.2.0" } } ``` ```bash yarn theme={null} # yarn uses resolutions - add manually to package.json: # "resolutions": { "@base-org/account": "2.2.0" } ``` ```bash bun theme={null} # bun supports overrides - add manually to package.json: # "overrides": { "@base-org/account": "2.2.0" } ``` Install the dependencies with your package manager of choice: ```bash npm theme={null} npm install viem wagmi @tanstack/react-query ``` ```bash pnpm theme={null} pnpm add viem wagmi @tanstack/react-query ``` ```bash yarn theme={null} yarn add viem wagmi @tanstack/react-query ``` ```bash bun theme={null} bun add viem wagmi @tanstack/react-query ``` **If this is not your first install** Make sure to delete your `node_modules` and `package-lock.json` and run a new install to ensure the overrides are applied. ## Configuration ### 1. Configure wagmi with Base Account Create your Wagmi configuration with the Base Account connector configured for Base Account: ```typescript app/wagmi.ts lines wrap expandable theme={null} import { cookieStorage, createConfig, createStorage, http } from "wagmi"; import { base, baseSepolia } from "wagmi/chains"; import { baseAccount } from "wagmi/connectors"; export function getConfig() { return createConfig({ chains: [base, baseSepolia], multiInjectedProviderDiscovery: false, connectors: [ baseAccount({ appName: "My Wagmi App", }), ], storage: createStorage({ storage: cookieStorage, }), ssr: true, transports: { [base.id]: http(), [baseSepolia.id]: http(), }, }); } declare module "wagmi" { interface Register { config: ReturnType; } } ``` ### 2. Wrap Your App Wrap your application with the Wagmi provider and QueryClient provider: ```tsx app/providers.tsx lines wrap expandable theme={null} 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { type ReactNode, useState } from 'react' import { type State, WagmiProvider } from 'wagmi' import { getConfig } from '@/wagmi' export function Providers(props: { children: ReactNode initialState?: State }) { const [config] = useState(() => getConfig()) const [queryClient] = useState(() => new QueryClient()) return ( ## Create a Simple Page (Sign in with Base) Create a simple landing page that uses Sign In With Base to authenticate the user) } ``` ```tsx app/layout.tsx lines wrap expandable theme={null} 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { type ReactNode, useState } from 'react' import { type State, WagmiProvider } from 'wagmi' import { getConfig } from '@/wagmi' export function Providers(props: { children: ReactNode initialState?: State }) { const [config] = useState(() => getConfig()) const [queryClient] = useState(() => new QueryClient()) return ( {props.children} ) } ``` {props.children} ```tsx app/components/SignInWithBase.tsx lines wrap expandable theme={null} "use client"; import { Connector } from "wagmi"; import { SignInWithBaseButton } from "@base-org/account-ui/react"; import { useState } from "react"; interface SignInWithBaseProps { connector: Connector; } export function SignInWithBase({ connector }: SignInWithBaseProps) { const [verificationResult, setVerificationResult] = useState (""); async function handleBaseAccountConnect() { const provider = await connector.getProvider(); if (provider) { try { // Generate a fresh nonce (this will be overwritten with the backend nonce) const clientNonce = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); console.log("clientNonce", clientNonce); // Connect with SIWE to get signature, message, and address const accounts = await (provider as any).request({ method: "wallet_connect", params: [ { version: "1", capabilities: { signInWithEthereum: { nonce: clientNonce, chainId: "0x2105", // Base Mainnet - 8453 }, }, }, ], }); // Verify the signature on the backend /* const walletAddress = accounts.accounts[0].address; const signature = accounts.accounts[0].capabilities.signInWithEthereum.signature; const message = accounts.accounts[0].capabilities.signInWithEthereum.message; const verifyResponse = await fetch("/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: walletAddress, message, signature, }), }); const result = await verifyResponse.json(); */ const result={success:true, address:accounts[0].address} // Mock response if (result.success) { setVerificationResult(`Verified! Address: ${result.address}`); } else { setVerificationResult(`Verification failed: ${result.error}`); } } catch (err) { console.error("Error:", err); setVerificationResult( `Error: ${err instanceof Error ? err.message : "Unknown error"}` ); } } else { console.error("No provider"); } } return ( ); } ``` ```tsx app/page.tsx lines wrap expandable theme={null} "use client"; import { useAccount, useConnect, useDisconnect } from "wagmi"; import { SignInWithBase } from "../components/SignInWithBase"; function App() { const account = useAccount(); const { connectors, connect, status, error } = useConnect(); const { disconnect } = useDisconnect(); return ( <>{verificationResult && ( {verificationResult})}Account
status: {account.status}{account.status === "connected" && ( )}
addresses: {JSON.stringify(account.addresses)}
chainId: {account.chainId}> ); } export default App; ```Connect
{connectors.map((connector) => { if (connector.name === "Base Account") { return (); } else { return ( ); } })} {status}{error?.message}This is a simple example to get you started. You will need to add your own backend logic to verify the signature and authenticate the user. You can find a complete example in the [Base Account Wagmi Template](https://github.com/base/demos/tree/master/base-account/base-account-wagmi-template). ## Run the wagmi App Run the application with your package manager of choice:```bash npm theme={null} npm run dev ``` ```bash pnpm theme={null} pnpm run dev ``` ```bash yarn theme={null} yarn run dev ``` ```bash bun theme={null} bun run dev ```
What you will see when you navigate to the page## Next Steps Now that you have Wagmi configured with Base Account, you can: * [Connect users with Sign in with Base](/sdks/base-account/framework-integrations/wagmi/sign-in-with-base) * [Access the Base Account provider](/sdks/base-account/framework-integrations/wagmi/other-use-cases) # Sign in with Base Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/sign-in-with-base Implement Base Account authentication using the proper SIWE flow with Wagmi Learn how to implement Sign in with Base using Wagmi by accessing the Base Account provider and following the proper SIWE (Sign-In With Ethereum) authentication flow. ## Prerequisites Make sure you have [set up Wagmi with Base Account](/sdks/base-account/framework-integrations/wagmi/setup) before following this guide. ## Overview To implement Sign in with Base with Wagmi, you need to: 1. Get the Base Account connector from Wagmi 2. Access the underlying provider from the connector 3. Use `wallet_connect` with `signInWithEthereum` capabilities 4. Verify the signature on your backend This follows the same flow as shown in the [authenticate users guide](/sdks/base-account/guides/authenticate-users), but integrates with Wagmi's connector system.To get access to the latest version of the Base Account SDK within Wagmi, you can use the following command to override it: ```bash Terminal theme={null} npm pkg set overrides.@base-org/account="latest" ``` Or you can use a specific version by adding the version to the overrides: ```bash Terminal theme={null} npm pkg set overrides.@base-org/account="2.2.0" ``` Make sure to delete your `node_modules` and `package-lock.json` and run a new install to ensure the overrides are applied. ## Implementation ### Code Snippets```ts Browser (Wagmi + SDK) lines wrap expandable theme={null} import { useState } from 'react' import { useConnect, useAccount, useDisconnect } from 'wagmi' import { baseAccount } from 'wagmi/connectors' export function SignInWithBase() { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState ### 3. Using the Pre-Built Button Component You can also use the official [Sign In With Base](/sdks/base-account/reference/ui-elements/sign-in-with-base-button) button component: ```tsx components/SignInButton.tsx lines wrap expandable theme={null} // components/SignInButton.tsx import { SignInWithBaseButton } from '@base-org/account-ui/react' import { useConnect } from 'wagmi' export function SignInButton() { const { connectAsync, connectors } = useConnect() const handleSignIn = async () => { const baseAccountConnector = connectors.find( connector => connector.id === 'baseAccount' ) if (!baseAccountConnector) return try { // Generate nonce const nonce = window.crypto.randomUUID().replace(/-/g, '') // Connect and get provider await connectAsync({ connector: baseAccountConnector }) const provider = baseAccountConnector.provider // Perform SIWE authentication const authResult = await provider.request({ method: 'wallet_connect', params: [{ version: '1', capabilities: { signInWithEthereum: { nonce, chainId: '0x2105' } } }] }) // Extract and verify signature const { accounts } = authResult const { address, capabilities } = accounts[0] const { message, signature } = capabilities.signInWithEthereum // Send to backend for verification await fetch('/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, message, signature }) }) } catch (error) { console.error('Authentication failed:', error) } } return ((null) const { isConnected, address } = useAccount() const { connectAsync, connectors } = useConnect() const { disconnect } = useDisconnect() // Find the Base Account connector const baseAccountConnector = connectors.find( connector => connector.id === 'baseAccount' ) const handleSignIn = async () => { if (!baseAccountConnector) { setError('Base Account connector not found') return } setIsLoading(true) setError(null) try { // 1 — get a fresh nonce (generate locally or prefetch from backend) const nonce = window.crypto.randomUUID().replace(/-/g, '') // OR prefetch from server // const nonce = await fetch('/auth/nonce').then(r => r.text()) // 2 — connect and get the provider await connectAsync({ connector: baseAccountConnector }) const provider = baseAccountConnector.provider // 3 — authenticate with wallet_connect const authResult = await provider.request({ method: 'wallet_connect', params: [{ version: '1', capabilities: { signInWithEthereum: { nonce, chainId: '0x2105' // Base Mainnet - 8453 } } }] }) const { accounts } = authResult const { address, capabilities } = accounts[0] const { message, signature } = capabilities.signInWithEthereum // 4 — verify on backend await fetch('/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, message, signature }) }) } catch (err: any) { console.error(`err ${err}`) setError(err.message || 'Sign in failed') } finally { setIsLoading(false) } } if (isConnected) { return ( {address}) } return ( ) } ``` ```ts Backend (Viem) lines wrap expandable theme={null} import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; const client = createPublicClient({ chain: base, transport: http() }); export async function verifySig(req, res) { const { address, message, signature } = req.body; const valid = await client.verifyMessage({ address, message, signature }); if (!valid) return res.status(401).json({ error: 'Invalid signature' }); // create session / JWT res.json({ ok: true }); } ```) } ``` **Please Follow the Brand Guidelines** If you intend on using the `SignInWithBaseButton`, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. # Using Sub Accounts Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/framework-integrations/wagmi/sub-accounts Implement Base Account Sub Accounts using Wagmi Learn how to create and manage Sub Accounts using Wagmi hooks and Base Account provider methods. ## Prerequisites Make sure you have: * [Set up Wagmi with Base Account](/sdks/base-account/framework-integrations/wagmi/setup) * [Implemented Sign in with Base](/sdks/base-account/framework-integrations/wagmi/sign-in-with-base) ## Overview Sub Accounts allow you to create child accounts that can spend from the parent account's balance using [Spend Permissions](/sdks/base-account/improve-ux/spend-permissions). This reduces the need for frequent user signatures and improves the user experience. ## Creating Sub Accounts Use the Base Account provider to create Sub Accounts: ```tsx CreateSubAccount.tsx lines wrap expandable theme={null} import { useAccount, useConnector } from 'wagmi' import { useState } from 'react' export function CreateSubAccount() { const { address, isConnected } = useAccount() const connector = useConnector() const [subAccount, setSubAccount] = useState(null) const [isCreating, setIsCreating] = useState(false) const createSubAccount = async () => { if (!connector || !isConnected) return setIsCreating(true) try { const provider = connector.provider // Create a new Sub Account const result = await provider?.request({ method: 'wallet_addSubAccount', params: [{ version: '1.0', chainId: `0x${Number(8453).toString(16)}`, // Base mainnet from: address, }] }) if (result?.subAccount) { setSubAccount(result.subAccount) console.log('Sub Account created:', result.subAccount) } } catch (error) { console.error('Failed to create Sub Account:', error) } finally { setIsCreating(false) } } return ( ) } ``` ## Listing Sub Accounts Retrieve existing Sub Accounts for the connected wallet: ```tsx SubAccountsList.tsx lines wrap expandable theme={null} import { useAccount, useConnector } from 'wagmi' import { useState, useEffect } from 'react' export function SubAccountsList() { const { address, isConnected } = useAccount() const connector = useConnector() const [subAccounts, setSubAccounts] = useStateSub Account Management
{!subAccount ? ( ) : ()}Sub Account created: {subAccount}
([]) const [isLoading, setIsLoading] = useState(false) const fetchSubAccounts = async () => { if (!connector || !isConnected) return setIsLoading(true) try { const provider = connector.provider const result = await provider?.request({ method: 'wallet_getSubAccounts', params: [{ version: '1.0', chainId: `0x${Number(8453).toString(16)}`, from: address, }] }) if (result?.subAccounts) { setSubAccounts(result.subAccounts) } } catch (error) { console.error('Failed to fetch Sub Accounts:', error) } finally { setIsLoading(false) } } useEffect(() => { if (isConnected) { fetchSubAccounts() } }, [isConnected, address]) return ( ) } ``` ## Using Sub Accounts for Transactions Once you have Sub Accounts, you can use them to perform transactions: ```tsx SubAccountTransactions.tsx lines wrap expandable theme={null} import { useAccount, useConnector, useWriteContract } from 'wagmi' import { useState } from 'react' export function SubAccountTransactions() { const { address } = useAccount() const connector = useConnector() const [selectedSubAccount, setSelectedSubAccount] = useState('') const [isLoading, setIsLoading] = useState(false) const sendTransactionFromSubAccount = async () => { if (!connector || !selectedSubAccount) return setIsLoading(true) try { const provider = connector.provider // Send transaction using Sub Account const result = await provider?.request({ method: 'wallet_sendCalls', params: [{ version: '1.0', chainId: `0x${Number(8453).toString(16)}`, from: selectedSubAccount, // Use Sub Account as sender calls: [{ to: '0x...' as `0x${string}`, value: '0x0', data: '0x' as `0x${string}` }] }] }) console.log('Transaction sent from Sub Account:', result) } catch (error) { console.error('Sub Account transaction failed:', error) } finally { setIsLoading(false) } } return ({subAccounts.length === 0 ? (Your Sub Accounts
No Sub Accounts found
) : ({subAccounts.map((subAccount, index) => ()}))}{subAccount} Sub Account #{index + 1}) } ``` ## Complete Example Here's a complete component that combines all Sub Account functionality: ```tsx SubAccountManager.tsx lines wrap expandable theme={null} import { useAccount, useConnector } from 'wagmi' import { useState, useEffect } from 'react' export function SubAccountManager() { const { address, isConnected } = useAccount() const connector = useConnector() const [subAccounts, setSubAccounts] = useStateSub Account Transactions
setSelectedSubAccount(e.target.value)} className="w-full p-2 border rounded" placeholder="0x... (Sub Account address)" />([]) const [isCreating, setIsCreating] = useState(false) const [isLoading, setIsLoading] = useState(false) const fetchSubAccounts = async () => { if (!connector || !isConnected) return setIsLoading(true) try { const provider = connector.provider const result = await provider?.request({ method: 'wallet_getSubAccounts', params: [{ version: '1.0', chainId: `0x${Number(8453).toString(16)}`, from: address, }] }) if (result?.subAccounts) { setSubAccounts(result.subAccounts) } } catch (error) { console.error('Failed to fetch Sub Accounts:', error) } finally { setIsLoading(false) } } const createSubAccount = async () => { if (!connector || !isConnected) return setIsCreating(true) try { const provider = connector.provider const result = await provider?.request({ method: 'wallet_addSubAccount', params: [{ version: '1.0', chainId: `0x${Number(8453).toString(16)}`, from: address, }] }) if (result?.subAccount) { // Refresh the list await fetchSubAccounts() } } catch (error) { console.error('Failed to create Sub Account:', error) } finally { setIsCreating(false) } } useEffect(() => { if (isConnected) { fetchSubAccounts() } }, [isConnected]) if (!isConnected) { return Please connect your wallet first
} return () } ``` ## Learn More * [Sub Accounts overview](/sdks/base-account/improve-ux/sub-accounts) * [Spend Permissions](/sdks/base-account/improve-ux/spend-permissions) * [Base Account RPC methods](/sdks/base-account/reference/core/provider-rpc-methods/wallet_addSubAccount) # Accept Payments Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/accept-payments Add one-tap USDC payments to your app with the pay() helper and Base Pay Button. ## Why Base Pay? USDC on Base is a fully-backed digital dollar that settles in seconds and costs pennies in gas. Base Pay lets you accept those dollars with a single click—no cards, no FX fees, no chargebacks. * **Any user can pay** – works with every Base Account (smart-wallet) out of the box. * **USDC, not gas** – you charge in dollars; gas sponsorship is handled automatically. * **Fast** – most payments confirm in \<2 seconds on Base. * **Funded accounts** – users pay with USDC from their Base Account or Coinbase Account. * **No extra fees** – you receive the full amount.Sub Account Manager
{subAccounts.length === 0 ? (Your Sub Accounts ({subAccounts.length})
No Sub Accounts found. Create one to get started!
) : ({subAccounts.map((subAccount, index) => ()}))}{subAccount}
Sub Account #{index + 1}
**Please Follow the Brand Guidelines** If you intend on using the BasePayButton, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. ## Client-side (Browser SDK)**Interactive Playground:** Try out the [`pay()`](/sdks/base-account/reference/base-pay/pay) and [`getPaymentStatus()`](/sdks/base-account/reference/base-pay/getPaymentStatus) functions in our [Base Pay SDK Playground](https://base.github.io/account-sdk/pay-playground) before integrating them into your app. ```ts Browser (SDK) lines wrap expandable theme={null} import { pay, getPaymentStatus } from '@base-org/account'; // Trigger a payment – user will see a popup from their wallet service try { const payment = await pay({ amount: '1.00', // USD amount (USDC used internally) to: '0xRecipient', // your address testnet: true // set false for Mainnet }); // Option 1: Poll until mined const { status } = await getPaymentStatus({ id: payment.id, testnet: true // MUST match the testnet setting used in pay() }); if (status === 'completed') console.log('🎉 payment settled'); } catch (error) { console.error(`Payment failed: ${error.message}`); } ```**Important:** The `testnet` parameter in [`getPaymentStatus()`](/sdks/base-account/reference/base-pay/getPaymentStatus) must match the value used in the original [`pay()`](/sdks/base-account/reference/base-pay/pay) call. If you initiated a payment on testnet with `testnet: true`, you must also pass `testnet: true` when checking its status. This is what the user will see when prompted to pay:### Collect user information (optional) Need an email, phone, or shipping address at checkout? Pass a
payerInfoobject: ```ts Collect User Information lines wrap expandable theme={null} try { const payment = await pay({ amount: '25.00', to: '0xRecipient', payerInfo: { requests: [ { type: 'email' }, { type: 'phoneNumber', optional: true }, { type: 'physicalAddress', optional: true } ], callbackURL: 'https://your-api.com/validate' // Optional - for server-side validation } }); console.log(`Payment sent! Transaction ID: ${payment.id}`); // Log the collected user information if (payment.payerInfoResponses) { if (payment.payerInfoResponses.email) { console.log(`Email: ${payment.payerInfoResponses.email}`); } if (payment.payerInfoResponses.phoneNumber) { console.log(`Phone: ${payment.payerInfoResponses.phoneNumber.number}`); console.log(`Country: ${payment.payerInfoResponses.phoneNumber.country}`); } if (payment.payerInfoResponses.physicalAddress) { const address = payment.payerInfoResponses.physicalAddress; console.log(`Shipping Address: ${address.name.firstName} ${address.name.familyName}, ${address.address1}, ${address.city}, ${address.state} ${address.postalCode}`); } } } catch (error) { console.error(`Payment failed: ${error.message}`); } ``` Supported request types: | type | returns | | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | |name| \{ firstName, familyName } | |phoneNumber| \{ number, country } | |physicalAddress| [full address object](/sdks/base-account/reference/core/capabilities/datacallback#physical-address-object) | |onchainAddress| string |Required by default — set optional: trueto avoid aborting the payment if the user declines.**How to validate the user's information?** You can use the `callbackURL` to validate the user's information on the server side. Learn more about this in the [callbackURL reference](/sdks/base-account/reference/core/capabilities/datacallback). ## Server Side When accepting payments, your backend must validate transactions and user info received from the frontend. This section covers two critical aspects: verifying transaction completion and validating user information. ### Verify User Transaction Use [`getPaymentStatus()`](/sdks/base-account/reference/base-pay/getPaymentStatus) on your backend to confirm that a payment has been completed before fulfilling orders. Never trust payment confirmations from the frontend alone. ```ts Backend (SDK) lines wrap expandable theme={null} import { getPaymentStatus } from '@base-org/account'; export async function checkPayment(txId: string, testnet = false) { const status = await getPaymentStatus({ id: txId, testnet // Must match the testnet setting from the original pay() call }); if (status.status === 'completed') { // fulfill order } } ```**Prevent Replay and Impersonation Attacks** * **Replay attacks:** A malicious user could submit the same valid transaction ID multiple times. Always track processed transaction IDs in your database. * **Impersonation attacks:** A malicious user could submit someone else's transaction ID to fulfill their own order. Always verify that the payment sender matches the authenticated user. Here's an example that prevents both attack vectors: ```ts Backend (with replay protection) lines wrap expandable theme={null} import { getPaymentStatus } from '@base-org/account'; // Example using a database to track processed transactions // Replace with your actual database implementation (PostgreSQL, MongoDB, etc.) const processedTransactions = new Map(); // In production, use a persistent database export async function verifyAndFulfillPayment( txId: string, orderId: string, payerAddress: string, // From authenticated user (SIWE, JWT, etc.) testnet = false ) { // 1. Check if this transaction was already processed if (processedTransactions.has(txId)) { throw new Error('Transaction already processed'); } // 2. Verify the payment status on-chain const { status, sender, amount, recipient } = await getPaymentStatus({ id: txId, testnet }); if (status !== 'completed') { throw new Error(`Payment not completed. Status: ${status}`); } // 3. Verify the payment sender matches the authenticated user // This prevents a malicious user from claiming someone else's payment if (sender.toLowerCase() !== payerAddress.toLowerCase()) { throw new Error('Payment sender does not match authenticated user'); } // 4. Validate the payment details match your order // This ensures the user paid the correct amount to the correct address const expectedAmount = await getOrderAmount(orderId); const expectedRecipient = process.env.PAYMENT_ADDRESS; if (amount !== expectedAmount) { throw new Error('Payment amount mismatch'); } if (recipient.toLowerCase() !== expectedRecipient.toLowerCase()) { throw new Error('Payment recipient mismatch'); } // 5. Mark transaction as processed BEFORE fulfilling // Store sender for easy lookup (e.g., to query all payments from a user) // In production, use a database transaction to ensure atomicity processedTransactions.set(txId, { orderId, sender, amount, timestamp: new Date() }); // 6. Fulfill the order await fulfillOrder(orderId); return { success: true, orderId, sender }; } ``` **Database recommendations for tracking transactions:** * Store the transaction ID, order ID, sender address, amount, timestamp, and fulfillment status * Use a unique constraint on the transaction ID to prevent duplicates * Consider adding an index on the transaction ID for fast lookups ### Validate User Info If you're collecting user information (email, phone, shipping address) during checkout, use the `callbackURL` parameter to validate this data server-side before the transaction is submitted. Your callback endpoint receives the user's information and must respond with either a success or error response: ```ts Backend (validation endpoint) lines wrap expandable theme={null} export async function POST(request: Request) { const requestData = await request.json(); const { requestedInfo } = requestData.capabilities.dataCallback; const errors: Record= {}; // Validate email if (requestedInfo.email) { const blockedDomains = ['tempmail.com', 'throwaway.com']; const domain = requestedInfo.email.split('@')[1]; if (blockedDomains.includes(domain)) { errors.email = 'Please use a valid email address'; } } // Validate shipping address if (requestedInfo.physicalAddress) { const addr = requestedInfo.physicalAddress; const supportedCountries = ['US', 'CA', 'GB']; if (!supportedCountries.includes(addr.countryCode)) { errors.physicalAddress = { countryCode: 'We currently only ship to US, Canada, and UK' }; } } // Return errors if validation failed if (Object.keys(errors).length > 0) { return Response.json({ errors }); } // Success - return the request to proceed with the transaction return Response.json({ request: requestData }); } ``` The callback is invoked **before** the transaction is submitted. If you return errors, the user is prompted to correct their information. If you return success, the transaction proceeds. For complete details on the callback request/response format and all supported data types, see the [dataCallback reference](/sdks/base-account/reference/core/capabilities/datacallback). ## Add the Base Pay Button Use the pre-built component for a native look-and-feel: ```tsx title="Checkout.tsx" lines wrap expandable theme={null} import { BasePayButton } from '@base-org/account-ui/react'; import { pay } from '@base-org/account'; export function Checkout() { const handlePayment = async () => { try { const payment = await pay({ amount: '5.00', to: '0xRecipient' }); console.log(`Payment sent! Transaction ID: ${payment.id}`); } catch (error) { console.error(`Payment failed: ${error.message}`); } }; return (); } ``` See full props and theming options in the [Button Reference](/sdks/base-account/reference/ui-elements/base-pay-button) and [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines). **Please Follow the Brand Guidelines** If you intend on using the BasePayButton, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. ## Test on Base Sepolia 1. Get test USDC from the Circle Faucet (select "Base Sepolia"). 2. Passtestnet: truein yourpay()andgetPaymentStatus()calls. 3. Use Sepolia BaseScan to watch the transaction. # Accept Recurring Payments Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/accept-recurring-payments Enable subscription-based revenue models with automatic USDC payments ## Start accepting recurring payments with Base Pay Subscriptions Base Subscriptions enable you to build predictable, recurring revenue streams by accepting automatic USDC payments. Whether you're running a SaaS platform, content subscription service, or any business model requiring regular payments, Base Subscriptions provide a seamless solution with no merchant fees. **Key Capabilities:**## How It Works Base Subscriptions leverage **Spend Permissions** – a powerful onchain primitive that allows users to grant revocable spending rights to applications. Here's the complete flow: Support any billing cycle that fits your business model: * Daily subscriptions for short-term services * Weekly for regular deliveries or services * Monthly for standard SaaS subscriptions * Annual for discounted long-term commitments * Custom periods (e.g., 14 days, 90 days) for unique models Charge any amount up to the permitted limit: * Fixed recurring amounts for predictable billing * Variable usage-based charges within a cap * Tiered pricing with different charge amounts * Prorated charges for mid-cycle changes Full control over the subscription lifecycle: * Real-time status checking to verify active subscriptions * Remaining charge amount for the current period * Next period start date for planning * Cancellation detection for immediate updates Built for production use cases: * No transaction fees or platform cuts * Instant settlement in USDC stablecoin * Testnet support for development and testing * Detailed transaction history for accounting * Programmatic access via SDK ## Implementation Guide ### Architecture Overview A complete subscription implementation requires both client and server components: **Client-Side (Frontend):** * User interface for subscription creation * Create wallet requests and handle user responses **Server-Side (Backend - Node.js):** * CDP smart wallet for executing charges and revocations * Scheduled jobs for periodic billing * Database for subscription tracking * Handlers for status updates * Retry logic for failed charges Your customer grants your application permission to charge their wallet up to a specified amount each billing period. This is a one-time approval that remains active until cancelled. Your backend service charges the subscription when payment is due, without requiring any user interaction. You can charge up to the approved amount per period. The spending limit automatically resets at the start of each new period. If you don't charge the full amount in one period, it doesn't roll over. Customers can view and cancel their subscriptions anytime through their wallet, ensuring transparency and trust. **CDP-Powered Backend** Base Subscriptions use **CDP (Coinbase Developer Platform) server wallets** for effortless backend management. The `charge()` and `revoke()` functions handle all transaction details automatically: * ✅ Automatic wallet management * ✅ Built-in transaction signing * ✅ Gas estimation and nonce handling * ✅ Optional paymaster support for gasless transactions Get CDP credentials from [CDP Portal](https://portal.cdp.coinbase.com/projects/api-keys). **Security Requirements** To accept recurring payments, you need: 1. CDP credentials (API key ID, secret, and wallet secret) 2. Backend infrastructure (Node.js) to execute charges securely 3. Database to store subscription IDs **bound to the authenticated user** (and their payer address) 4. Never expose CDP credentials in client-side code 5. Never charge an arbitrary subscription `id` from the browser alone — look up the ID from your database, and pass `expectedPayer` to `charge()` / `revoke()` ### Setup: Create Your Subscription Owner Wallet First, set up your CDP smart wallet that will act as the subscription owner: ```typescript backend/setup.ts lines wrap expandable theme={null} import { base } from '@base-org/account/node'; // Backend setup (Node.js only) // Set CDP credentials as environment variables: // CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET // PAYMASTER_URL (recommended for gasless transactions) async function setupSubscriptionWallet() { try { // Create or retrieve your subscription owner wallet (CDP smart wallet) const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({ walletName: 'my-app-subscriptions' // Optional: customize wallet name }); console.log('✅ Subscription owner wallet ready!'); console.log(`Smart Wallet Address: ${wallet.address}`); console.log(`Wallet Name: ${wallet.walletName}`); // Make this address available to your frontend // Option 1: Store in database/config // Option 2: Expose via API endpoint // Option 3: Set as public environment variable (e.g., NEXT_PUBLIC_SUBSCRIPTION_OWNER) return wallet; } catch (error) { console.error('Failed to setup wallet:', error.message); throw error; } } // Run once at application startup setupSubscriptionWallet(); // Optional: Provide an API endpoint for the frontend to fetch the address export async function getSubscriptionOwnerAddress() { const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet(); return wallet.address; } ```**Backend Only**: This setup runs in your Node.js backend with CDP credentials. The resulting wallet address is public and safe to share with your frontend for use in `subscribe()` calls. **Keep CDP Credentials Private**: Never expose CDP credentials (API key, secrets) to the frontend. Only the subscription owner wallet address needs to be accessible to the frontend. ### Client-Side: Create Subscriptions Users create subscriptions from your frontend application: ```tsx SubscriptionButton.tsx lines wrap expandable theme={null} import React, { useState } from 'react'; import { base } from '@base-org/account'; // This address comes from your backend setup (see setup.ts example above) // You can fetch it from your backend or configure it as a public env var const SUBSCRIPTION_OWNER_ADDRESS = "0xYourCDPWalletAddress"; // Replace with your actual address export function SubscriptionButton() { const [loading, setLoading] = useState(false); const [subscribed, setSubscribed] = useState(false); const [subscriptionId, setSubscriptionId] = useState(''); const handleSubscribe = async () => { setLoading(true); try { // Create subscription const subscription = await base.subscription.subscribe({ recurringCharge: "29.99", subscriptionOwner: SUBSCRIPTION_OWNER_ADDRESS, // Address from your backend CDP wallet periodInDays: 30, testnet: false }); // Store subscription ID for future reference setSubscriptionId(subscription.id); console.log('Subscription created:', subscription.id); console.log('Payer:', subscription.subscriptionPayer); console.log('Amount:', subscription.recurringCharge); console.log('Period:', subscription.periodInDays, 'days'); // Send subscription ID to your backend await saveSubscriptionToBackend(subscription.id, subscription.subscriptionPayer); setSubscribed(true); } catch (error) { console.error('Subscription failed:', error); alert('Failed to create subscription: ' + error.message); } finally { setLoading(false); } }; const saveSubscriptionToBackend = async (id: string, payer: string) => { // Example API call to store subscription in your database const response = await fetch('/api/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subscriptionId: id, payerAddress: payer }) }); if (!response.ok) { throw new Error('Failed to save subscription'); } }; if (subscribed) { return (); } return ( ); } ``` ### Server-Side: Charge Subscriptions Execute charges effortlessly from your backend using CDP: ```typescript chargeSubscriptions.ts lines wrap expandable theme={null} import { base } from '@base-org/account/node'; // Requires: CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET env vars // Recommended: PAYMASTER_URL for gasless transactions async function chargeSubscription( subscriptionId: string, payerAddress: string, recipientAddress?: string ) { try { // 1. Check subscription status const status = await base.subscription.getStatus({ id: subscriptionId, testnet: false }); if (!status.isSubscribed) { console.log('Subscription cancelled by user'); return { success: false, reason: 'cancelled' }; } const availableCharge = parseFloat(status.remainingChargeInPeriod || '0'); if (availableCharge === 0) { console.log(`No charge available until ${status.nextPeriodStart}`); return { success: false, reason: 'no_charge_available' }; } // 2. Charge the subscription - CDP handles everything automatically // Use the server-stored id + payerAddress from your database (not browser input alone) // Using paymaster for gasless transactions (recommended) const result = await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', expectedPayer: payerAddress, paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions recipient: recipientAddress, // Optional: send USDC to specific address testnet: false }); console.log(`✅ Charged ${result.amount} USDC (gasless)`); console.log(`Transaction: ${result.id}`); if (recipientAddress) { console.log(`Sent to: ${recipientAddress}`); } return { success: true, transactionHash: result.id, amount: result.amount, recipient: result.recipient }; } catch (error) { console.error('Charge failed:', error); return { success: false, error: error.message }; } } ``` ### Server-Side: Revoke Subscriptions Cancel subscriptions programmatically from your backend: ```typescript revokeSubscription.ts lines wrap expandable theme={null} import { base } from '@base-org/account/node'; async function revokeSubscription( subscriptionId: string, payerAddress: string, reason: string ) { try { // Revoke the subscription with paymaster for gasless transactions const result = await base.subscription.revoke({ id: subscriptionId, expectedPayer: payerAddress, paymasterUrl: process.env.PAYMASTER_URL, // Optional: for gasless transactions testnet: false }); console.log(`✅ Revoked subscription: ${subscriptionId}`); console.log(`Transaction: ${result.id}`); console.log(`Reason: ${reason}`); return { success: true, transactionHash: result.id }; } catch (error) { console.error('Revoke failed:', error); return { success: false, error: error.message }; } } // Usage examples async function handleUserCancellation(subscriptionId: string, payerAddress: string) { return await revokeSubscription(subscriptionId, payerAddress, 'user_requested'); } async function handlePolicyViolation(subscriptionId: string, payerAddress: string) { return await revokeSubscription(subscriptionId, payerAddress, 'policy_violation'); } ```✅ Subscription active Subscription ID: {subscriptionId.slice(0, 10)}...
**Automatic Transaction Management**: The `charge()` and `revoke()` functions handle all transaction details including wallet management, gas estimation, nonce handling, and transaction confirmation. Use the `paymasterUrl` parameter to enable gasless transactions for your users. **Gasless Transactions**: Set the `PAYMASTER_URL` environment variable to sponsor gas fees for your subscription charges and revocations. This creates a seamless experience where your backend covers all gas costs. Get your paymaster URL from the [CDP Portal](https://portal.cdp.coinbase.com/). ### Fund Management By default, charged USDC remains in your subscription owner wallet. You can optionally specify a `recipient` address to automatically transfer funds to a different address:### Testing on Testnet Test your subscription implementation on Base Sepolia before going live: ```typescript testnet-frontend.ts expandable theme={null} // Frontend: Create subscription on testnet const subscription = await base.subscription.subscribe({ recurringCharge: "10.00", subscriptionOwner: SUBSCRIPTION_OWNER_ADDRESS, periodInDays: 1, // Daily for faster testing testnet: true // Use Base Sepolia }); ``` ```typescript testnet-backend.ts lines wrap expandable theme={null} // Backend: Setup wallet on testnet (Node.js only) import { base } from '@base-org/account/node'; const wallet = await base.subscription.getOrCreateSubscriptionOwnerWallet({ walletName: 'testnet-subscriptions' }); // Check status on testnet const status = await base.subscription.getStatus({ id: subscriptionId, testnet: true }); // Charge on testnet with paymaster const result = await base.subscription.charge({ id: subscriptionId, amount: "10.00", paymasterUrl: process.env.PAYMASTER_URL, // Gasless transactions testnet: true }); console.log(`Testnet charge (gasless): ${result.id}`); ``` ## Network and Token Support **Base Subscriptions (USDC on Base):** | Network | Chain ID | Token | Status | | ------------ | -------- | ----- | ------------------- | | Base Mainnet | 8453 | USDC | ✅ Production Ready | | Base Sepolia | 84532 | USDC | ✅ Testing Available | ```typescript Keep Funds in Owner Wallet lines wrap expandable theme={null} // Funds stay in the subscription owner wallet const result = await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', testnet: false }); // USDC is now in your CDP smart wallet // Access it later or transfer as needed ``` ```typescript Send to Treasury Wallet lines wrap expandable theme={null} // Automatically send to your treasury wallet const result = await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', recipient: '0xYourTreasuryAddress', testnet: false }); // USDC is sent directly to the recipient address console.log(`Sent ${result.amount} to ${result.recipient}`); ``` ```typescript Dynamic Recipients lines wrap expandable theme={null} // Send to different addresses based on subscription type async function chargeWithRecipient(subscriptionId: string, plan: string) { const recipients = { premium: '0xPremiumTreasuryAddress', basic: '0xBasicTreasuryAddress', enterprise: '0xEnterpriseTreasuryAddress' }; return await base.subscription.charge({ id: subscriptionId, amount: 'max-remaining-charge', recipient: recipients[plan], testnet: false }); } ``` **Custom Implementations Possible**: While Base Subscriptions are optimized for USDC on Base, you can use the underlying [Spend Permissions](/sdks/base-account/improve-ux/spend-permissions) primitive to build custom subscription implementations with any ERC-20 token or native ETH on any EVM-compatible chain. ## Advanced Topics ### Custom Transaction Handling For developers who need manual control over transaction execution or want to integrate with existing wallet infrastructure, use the lower-level utilities:## API Reference If you can't use CDP wallets, `prepareCharge()` gives you call data to execute manually: ```typescript Manual Charge with prepareCharge lines wrap expandable theme={null} import { base } from '@base-org/account'; // Prepare charge call data const chargeCalls = await base.subscription.prepareCharge({ id: subscriptionId, amount: 'max-remaining-charge', testnet: false, expectedSpender: subscriptionOwner, expectedPayer: payerAddress, }); // Execute with your own wallet infrastructure // (requires custom wallet client setup) ``` See [`prepareCharge` reference](/sdks/base-account/reference/base-pay/prepareCharge) for details. Similarly, `prepareRevoke()` provides revocation call data: ```typescript Manual Revoke with prepareRevoke lines wrap expandable theme={null} import { base } from '@base-org/account'; // Prepare revoke call data const revokeCall = await base.subscription.prepareRevoke({ id: subscriptionId, testnet: false, expectedSpender: subscriptionOwner, expectedPayer: payerAddress, }); // Execute with your own wallet infrastructure ``` See [`prepareRevoke` reference](/sdks/base-account/reference/base-pay/prepareRevoke) for details. Create subscriptions from frontend Check subscription status Charge subscriptions from backend Cancel subscriptions from backend Setup CDP owner wallet for subscription management Advanced: Custom charge execution Advanced: Custom revoke execution Deep dive into the underlying primitive Accept single payments # Authenticate Users Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/authenticate-users Let a user click “Sign in with Base,” prove ownership of their onchain account, and give your server everything it needs to create a session – using open standards and no passwords ## Why Wallet Signatures Instead of Passwords? 1. **No new passwords** – authentication happens with the key the user already controls. 2. **Nothing to steal or reuse** – each login is a one-off, domain-bound signature that never leaves the user’s device. 3. **Wallet-agnostic** – works in any EIP-1193 wallet (browser extension, mobile deep-link, embedded provider) and follows the open ["Sign in with Ethereum" (SIWE) EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) standard. Base Accounts build on those standards so you can reuse any SIWE tooling – while still benefiting from passkeys, session keys, and smart-wallet security. **Please Follow the Brand Guidelines** If you intend on using the `SignInWithBaseButton`, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. **Do you prefer video content?** There is a video guide that covers the implementation in detail in the [last section of this page](#video-guide). ## High-Level Flow ```mermaid Authentication Flow lines wrap expandable theme={null} sequenceDiagram participant User participant Browser participant AppServer as "App Server" participant SDK participant Account alt Generate locally Browser->>Browser: randomNonce() else Prefetch Browser->>AppServer: GET /auth/nonce (on page load) AppServer-->>Browser: nonce end User->>Browser: Click "Sign in with Base" Browser->>SDK: wallet_connect(signInWithEthereum {nonce}) SDK->>Account: wallet_connect(...) User->>Account: Approve connection Account-->>SDK: {address, message, signature} SDK-->>Browser: {address, message, signature} Browser-->>AppServer: POST /auth/verify {address, message, signature} AppServer-->>Browser: session token / JWT ```**Undeployed Smart Wallets?** Base Account signatures include the [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492) wrapper so they can be verified even before the wallet contract is deployed. Viem’s [`verifyMessage`](https://viem.sh/docs/actions/public/verifyMessage) and [`verifyTypedData`](https://viem.sh/docs/actions/public/verifyTypedData) handle this automatically. ## Implementation ### Install Dependencies Make sure to install the dependencies:```bash npm theme={null} npm install @base-org/account @base-org/account-ui ``` ```bash pnpm theme={null} pnpm add @base-org/account @base-org/account-ui ``` ```bash yarn theme={null} yarn add @base-org/account @base-org/account-ui ``` ```bash bun theme={null} bun add @base-org/account @base-org/account-ui ``` ### Code Snippets```ts Browser (SDK) lines wrap expandable theme={null} import { createBaseAccountSDK } from "@base-org/account"; // Initialize the SDK const provider = createBaseAccountSDK({ appName: "My App", }).getProvider(); // 1 — get a fresh nonce (generate locally or prefetch from backend) const nonce = window.crypto.randomUUID().replace(/-/g, ""); // OR prefetch from server // const nonce = await fetch("/auth/nonce").then((response) => response.text()); // 2 — switch to Base Chain const switchChainResponse = await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x2105" }], }); console.log("Switch chain response:", switchChainResponse); // 3 — connect and authenticate try { const { accounts } = await provider.request({ method: "wallet_connect", params: [ { version: "1", capabilities: { signInWithEthereum: { nonce, chainId: "0x2105", // Base Mainnet - 8453 }, }, }, ], }); const { address } = accounts[0]; const { message, signature } = accounts[0].capabilities.signInWithEthereum; await fetch("/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address, message, signature }), }); } catch (error) { console.error("Failed to authenticate with Base Account:", error); } ``` ```ts Backend (Viem) lines wrap expandable theme={null} import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; const client = createPublicClient({ chain: base, transport: http() }); export async function verifySig(req, res) { const { address, message, signature } = req.body; const valid = await client.verifyMessage({ address, message, signature }); if (!valid) return res.status(401).json({ error: 'Invalid signature' }); // create session / JWT res.json({ ok: true }); } ``` If using the above code beyond Base Account, note that not every wallet supports the new [ wallet\_connectmethod](/sdks/base-account/reference/core/provider-rpc-methods/wallet_connect) yet. If the call throws \[method\_not\_supported], fall back to usingeth\_requestAccountsandpersonal\_sign.To avoid [popup blockers](/sdks/base-account/more/troubleshooting/usage-details/popups#default-blocking-behavior), fetch or generate the nonce before the user presses "Sign in with Base" (for example on page load). For security, the only requirement is that your backend keeps track of every nonce and refuses any that are reused – regardless of where it originated. ### Example Express Server ```ts title="server/auth.ts" lines wrap expandable theme={null} import crypto from "crypto"; import express from "express"; import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; const app = express(); app.use(express.json()); // Simple in-memory nonce store (swap for Redis or DB in production) const nonces = new Set(); app.get("/auth/nonce", (_, res) => { const nonce = crypto.randomBytes(16).toString("hex"); nonces.add(nonce); res.send(nonce); }); const client = createPublicClient({ chain: base, transport: http() }); app.post("/auth/verify", async (req, res) => { const { address, message, signature } = req.body; // 1. Check nonce hasn\'t been reused const nonce = message.match(/at (\w{32})$/)?.[1]; if (!nonce || !nonces.delete(nonce)) { return res.status(400).json({ error: "Invalid or reused nonce" }); } // 2. Verify signature const valid = await client.verifyMessage({ address, message, signature }); if (!valid) return res.status(401).json({ error: "Invalid signature" }); // 3. Create session / JWT here res.json({ ok: true }); }); app.listen(3001, () => console.log("Auth server listening on :3001")); ``` ## Add the Base Sign in with Base Button Use the pre-built component for a native look-and-feel: ```tsx title="App.tsx" lines wrap expandable theme={null} import { SignInWithBaseButton } from "@base-org/account-ui/react"; export function App() { return ( signInWithBase()} /> ); } ``` See full props and theming options in the [Button Reference](/sdks/base-account/reference/ui-elements/sign-in-with-base-button) and [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines). **Please Follow the Brand Guidelines** If you intend on using the `SignInWithBaseButton`, please follow the [Brand Guidelines](/sdks/base-account/reference/ui-elements/brand-guidelines) to ensure consistency across your application. ## Video Guide # Migrate from Coinbase Wallet SDK Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/migration-guide A guide to migrating from the Coinbase Wallet SDK to the Base Account SDK ## Overview The Base Account SDK allows Base Account users to connect 3rd party mobile and web applications. The Base Account SDK is the successor to the Coinbase Wallet SDK, which is now considered legacy. Developers should integrate the Base Account SDK such that users connect to use it via a "Sign in with Base" or "Base" button. We do not recommend immediately replacing existing "Coinbase Wallet" buttons in your app, but rather add the Base Account button as an additional option, next to a "Coinbase Wallet" button. This will allow a transition period for users to get familiar with the new Base brand. Driving this change is a transition of our mobile app: the Coinbase Wallet app is now the Base app. We are gradually transitioning all of our users to have Base Accounts, which are powered by our Smart Wallet platform. Below is a table of existing users and how they will connect to apps now and in the future: | User Type | Today | Future (\~Fall 2025) | | --------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- | | Smart Wallet users (web and mobile app) | Automatically have a Base Account, can use "Sign in with Base" | No change | | New Base app users | Automatically have a Base Account, can use "Sign in with Base" | No change | | Coinbase Wallet Extension Users | Should continue to connect with "Coinbase Wallet" button | Will have a path to migrate to Base Account and use "Sign in with Base" | | Coinbase Wallet mobile app EOA users | Should continue to connect with "Coinbase Wallet" button | Will have a path to migrate to Base Account and use "Sign in with Base" | ## Changes to User Experience When Click "Coinbase Wallet" As of SDK v4.0, users without Coinbase Wallet extension are directed to a popup window where they can choose to connect with the mobile app, via WalletLink, or use a passkey-powered Smart Wallet natively on the web. This will continue to be the case, but the logged out experience has changed to educate Smart Wallet users that they should be using "Sign in with Base" in the future. Here is how it looks on desktop.
Desktop experience showing the updated logged out flow for Coinbase Wallet usersIf you would like to avoid users seeing any popup window, we recommend using a version of the Coinbase Wallet SDK \< 4.0. You can find the latest version on the [Coinbase Wallet SDK releases page](https://github.com/coinbase/coinbase-wallet-sdk/releases). ## How to Migrate? If you're using the SDK, you can simply replace the Coinbase Wallet SDK with the Base Account SDK. ```tsx Base Account SDK Setup theme={null} import { createBaseAccountSDK } from "@base-org/account"; const baseAccount = createBaseAccountSDK({ // ... }); ``` For more information please refer to the [Quickstart](/sdks/base-account/quickstart/web) guide. If you're using a third party library, you can follow the [Wagmi](/sdks/base-account/framework-integrations/wagmi/setup), [RainbowKit](/sdks/base-account/framework-integrations/rainbowkit) or [Privy](/sdks/base-account/framework-integrations/privy/setup) guides. We will have a more complete migration guide in the near future. # Sign and Verify Typed Data Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/sign-and-verify-typed-data EIP-712 structured data signing and verification for Base Account ## Overview Base Account uses [Smart Wallet contracts](https://github.com/coinbase/smart-wallet) under the hood. Smart contract wallets introduce a few differences in how messages are signed compared to traditional Externally Owned Accounts (EOAs). This guide explains how to properly implement message signing using Base Account, covering both standard messages and typed data signatures, as well as some edge cases. ## Introduction Before walking through the details of how to sign and verify messages using Base Account, it's important to understand some of the use cases of signing messages with wallets, as well as the key differences between EOAs and smart contracts when it comes to signing messages. ### Use Cases for Wallet Signatures Blockchain-based apps use wallet signatures for two main categories: 1. **Signatures for offchain verification**: Used for authenticating users in onchain apps (e.g., Sign-In with Ethereum) to avoid spoofing. The signature is not used for any onchain action. 2. **Signatures for onchain verification**: Used for signing onchain permissions (e.g., [Permit2](https://github.com/Uniswap/permit2)) or batching transactions. The signature is usually stored for future transactions. ### Smart Contract Wallet Differences Smart contract wallets handle signatures differently from EOAs in several ways: * The contract itself doesn't produce signatures - instead, the owner (e.g., passkey) signs messages * Verification happens through the `isValidSignature` function defined in [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) * Smart contract wallet addresses are often deterministic, allowing signature support before deployment via [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492) ## High-Level Flow In this guide, we'll walk through the high-level flow of signing and verifying messages using Base Account. ```mermaid Signing Flow lines wrap expandable theme={null} sequenceDiagram participant User participant Browser participant AppServer as "App Server" participant SDK participant Account User->>Browser: Trigger signing action Browser->>AppServer: GET /typed-data/prepare AppServer-->>Browser: EIP-712 payload Browser->>SDK: eth_signTypedData_v4 SDK->>Account: eth_signTypedData_v4(payload) User->>Account: Review and approve signature Account-->>SDK: signature SDK-->>Browser: signature Browser-->>AppServer: POST /typed-data/verify {payload, signature} AppServer-->>Browser: verification result ``` ## Implementation For the purposes of this guide, we'll use a simple example of a typed data payload that contains a permission to spend user's funds (see [Spend Permissions](/sdks/base-account/improve-ux/spend-permissions)) ### Code Snippets```ts Browser (SDK) lines wrap expandable theme={null} import { createBaseAccountSDK } from "@base-org/account"; // Initialize the SDK const provider = createBaseAccountSDK().getProvider(); // 1 — Prepare the typed data payload const typedData = { domain: { name: 'Spend Permission Manager', version: '1', chainId: 8453, // or any other supported chain verifyingContract: SPEND_PERMISSION_MANAGER_ADDRESS, }, types: { SpendPermission: [ { name: 'account', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'token', type: 'address' }, { name: 'allowance', type: 'uint160' }, { name: 'period', type: 'uint48' }, { name: 'start', type: 'uint48' }, { name: 'end', type: 'uint48' }, { name: 'salt', type: 'uint256' }, { name: 'extraData', type: 'bytes' }, ], }, primaryType: 'SpendPermission', message: spendPermissionData, }; // 2 — Request signature from user try { const accounts = await provider.request({ method: 'eth_requestAccounts' }); const signature = await provider.request({ method: 'eth_signTypedData_v4', params: [accounts[0], JSON.stringify(typedData)] }); // 3 — Send to backend for verification const response = await fetch('/typed-data/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ typedData, signature, address: accounts[0] }) }); const result = await response.json(); console.log('Verification result:', result); } catch (err) { console.error('Signing failed:', err); } ``` ```ts Backend (Viem) lines wrap expandable theme={null} import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; const client = createPublicClient({ chain: base, transport: http() }); export async function verifyTypedData(req, res) { const { typedData, signature, address } = req.body; try { // Verify the typed data signature const valid = await client.verifyTypedData({ address, domain: typedData.domain, types: typedData.types, primaryType: typedData.primaryType, message: typedData.message, signature }); if (!valid) { return res.status(401).json({ error: 'Invalid signature' }); } // Additional validation logic here // e.g., check expiry, nonce, permissions, etc. const now = Math.floor(Date.now() / 1000); if (typedData.message.expiry < now) { return res.status(401).json({ error: 'Signature expired' }); } // Process the verified typed data res.json({ valid: true, message: 'Signature verified successfully', data: typedData.message }); } catch (error) { console.error('Verification error:', error); res.status(500).json({ error: 'Verification failed' }); } } ``` ## Example Express Server ```ts title="server/typed-data.ts" lines wrap expandable theme={null} import express from 'express'; import { createPublicClient, http } from 'viem'; import { base } from 'viem/chains'; const app = express(); app.use(express.json()); const client = createPublicClient({ chain: base, transport: http() }); // Simple nonce store (use Redis/DB in production) const usedNonces = new Set(); app.get('/typed-data/prepare', (req, res) => { const { userAddress, action, resource } = req.query; const nonce = Math.floor(Math.random() * 1000000); const expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour const typedData = { // YOUR TYPED DATA HERE } res.json(typedData); }); app.post('/typed-data/verify', async (req, res) => { const { typedData, signature, address } = req.body; try { // 1. Check nonce hasn't been reused const nonceKey = `${address}-${typedData.message.nonce}`; if (usedNonces.has(nonceKey)) { return res.status(400).json({ error: 'Nonce already used' }); } // 2. Check expiry const now = Math.floor(Date.now() / 1000); if (typedData.message.expiry < now) { return res.status(400).json({ error: 'Signature expired' }); } // 3. Verify signature const valid = await client.verifyTypedData({ address, domain: typedData.domain, types: typedData.types, primaryType: typedData.primaryType, message: typedData.message, signature }); if (!valid) { return res.status(401).json({ error: 'Invalid signature' }); } // 4. Mark nonce as used usedNonces.add(nonceKey); // 5. Process the verified action res.json({ valid: true, message: 'Typed data verified successfully', action: typedData.message.action, resource: typedData.message.resource }); } catch (error) { console.error('Verification error:', error); res.status(500).json({ error: 'Verification failed' }); } }); app.listen(3001, () => console.log('Typed data server listening on :3001')); ``` ## Best Practices ### Domain Separation Always use unique domain parameters to prevent signature replay across different applications: ```tsx Domain Separation theme={null} const domain = { name: 'Your App Name', // Unique app identifier version: '1', // Version your types chainId: 8453, // Network-specific verifyingContract: contractAddr // Contract that will verify }; ``` ### Nonce Management Include nonces to prevent replay attacks: ```tsx Nonce Management theme={null} // Generate unique nonces const nonce = crypto.randomBytes(16).toString('hex'); // Store and validate nonces server-side const usedNonces = new Set(); // Use Redis/DB in production ``` ### Expiry Times Always include expiry timestamps for time-bound signatures: ```tsx Expiry Times theme={null} const expiry = Math.floor(Date.now() / 1000) + 3600; // 1 hour ``` # Transaction Simulation Data Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/tips/inspect-txn-simulation Copy Base Account transaction simulation request and response data to inspect it in a text editor. There is a hidden feature which enables you to easily copy transaction simulation request and response data which can then be pasted it in a text editor to inspect. ## Instructions * Click the area defined in red ***five times***, then paste the copied data in a text editor. # Popup Tips Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/guides/tips/popup-tips Practical tips for working with Base Account popups, including reliable triggering and common pitfalls. ## Overview When a Base Account is connected and Coinbase Wallet SDK receives a request, it opens [keys.coinbase.com](https://keys.coinbase.com/) in a popup window and passes the request to the popup for handling. Keep the following points in mind when working with the Base Account popup. ## Default Blocking Behavior * Most modern browsers block all popups by default, unless they are triggered by a click. * If a popup is blocked the browser shows a notification to the user, allowing them to manage popup settings. ### What to Do About It * Ensure there is no additional logic between the button click and the request to open the Base Account popup, as browsers might perceive the request as programmatically initiated. * If logic is unavoidable, keep it minimal and test thoroughly in all supported browsers. ## `Cross-Origin-Opener-Policy` If the Base Account popup opens and displays an error or infinite spinner, it may be due to the dapp's `Cross-Origin-Opener-Policy`. Be sure to use a directive that allows the Base Account popup to function. * ✅ Allows Base Account popup to function * `unsafe-none` (default) * `same-origin-allow-popups` (recommended) * ❌ Breaks Base Account popup * `same-origin` For more detailed information refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy). ## Base Account Popup 'Linger' Behavior * Sometimes a dapp may programmatically make a followup request based on the response to a previous request. Normally, browsers block these programmatic requests to open popups. * To address this, after the Base Account popup responds to a request, it will linger for 200ms to listen for another incoming request before closing. * If a request is received *during* this 200ms window, it will be received and handled within the same popup window. * If a request is received *after* the 200ms window and the popup has closed, opening the Base Account popup will be blocked by the browser. # Batch Transactions Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/improve-ux/batch-transactions Send multiple onchain calls in a single Base Account transaction to reduce multi-step flows to one click. With Base Account, you can send multiple onchain calls in a single transaction. Doing so improves the UX of multi-step interactions by reducing them to a single click. A common example of where you might want to leverage batch transactions is an ERC-20 `approve` followed by a swap. You can submit batch transactions by using the `wallet_sendCalls` RPC method, defined in [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792).![]()
**Do you prefer video content?** There is a video guide that covers the implementation in detail in the [last section of this page](#video-guide). ## Installation Install the Base Account SDK:```bash npm theme={null} npm install @base-org/account ``` ```bash pnpm theme={null} pnpm add @base-org/account ``` ```bash yarn theme={null} yarn add @base-org/account ``` ```bash bun theme={null} bun add @base-org/account ``` ## Setup ### Initialize the SDK Import and create the Base Account SDK instance: ```tsx batchTransactions.tsx lines wrap expandable theme={null} import { createBaseAccountSDK } from "@base-org/account"; const sdk = createBaseAccountSDK({ appName: "Base Account SDK Demo", appLogoUrl: "https://base.org/logo.png", }); const provider = sdk.getProvider(); ``` ## Basic Batch Transaction ### Simple Multiple Transfers Send multiple ETH transfers in a single transaction: ```tsx batchTransactions.tsx lines wrap expandable theme={null} import { createBaseAccountSDK, getCryptoKeyAccount } from "@base-org/account"; import { numberToHex, parseEther } from "viem"; const sdk = createBaseAccountSDK({ appName: "Batch Transaction Demo", appLogoUrl: "https://base.org/logo.png", }); const provider = sdk.getProvider(); async function sendBatchTransfers() { try { // Get crypto account const cryptoAccount = await getCryptoKeyAccount(); const fromAddress = cryptoAccount?.account?.address; // Prepare batch calls const calls = [ { to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", value: numberToHex(parseEther("0.001")), // 0.001 ETH data: "0x", // Empty data for simple transfer }, { to: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E456", value: numberToHex(parseEther("0.001")), // 0.001 ETH data: "0x", // Empty data for simple transfer }, ]; // Send batch transaction const result = await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0.0", from: fromAddress, chainId: numberToHex(base.constants.CHAIN_IDS.base), atomicRequired: true, // All calls must succeed or all fail calls: calls, }, ], }); console.log("Batch transaction sent:", result); return result; } catch (error) { console.error("Batch transaction failed:", error); throw error; } } ``` ## Contract Interactions ### ERC-20 Approve and Mint an NFT (ERC-721) A common pattern is to approve the NFT contract to move your ERC-20 and then mint an NFT (ERC-721): ```tsx batchTransactions.tsx lines wrap expandable theme={null} import { createBaseAccountSDK, getCryptoKeyAccount, base, } from "@base-org/account"; import { numberToHex, parseUnits, encodeFunctionData } from "viem"; // ERC-20 ABI for approve const erc20Abi = [ { inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], name: "approve", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function", }, ] as const; // ERC721 ABI for the mint function const erc721Abi = [ { inputs: [ { name: "to", type: "address" }, { name: "tokenId", type: "uint256" }, ], name: "mint", outputs: [], stateMutability: "nonpayable", type: "function", }, ] as const; // USDC contract address on Base Sepolia const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // NFT contract address on Base Sepolia const NFT_CONTRACT_ADDRESS = "0x82039e7C37D7aAc98D0F4d0A762F4E0d8c8DC273"; async function approveAndTransfer() { const sdk = createBaseAccountSDK({ appName: "ERC-20 Batch Demo", appLogoUrl: "https://base.org/logo.png", }); const provider = sdk.getProvider(); const cryptoAccount = await getCryptoKeyAccount(); const fromAddress = cryptoAccount?.account?.address; // Encode the first approve call - approve USDC to NFT contract const call1Data = encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [ NFT_CONTRACT_ADDRESS, parseUnits("1000", 6), // USDC has 6 decimals ], }); // Encode the second call - mint NFT to the user's address const call2Data = encodeFunctionData({ abi: erc721Abi, functionName: "mint", args: [fromAddress as `0x${string}`, BigInt("1")], }); const result = await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0.0", from: fromAddress, chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia), atomicRequired: true, calls: [ { to: USDC_ADDRESS, data: call1Data, }, { to: NFT_CONTRACT_ADDRESS, data: call2Data, }, ], }, ], }); return result; } ``` ## Advanced Features ### Checking Wallet Capabilities Before sending batch transactions, you can check if the wallet supports atomic batching: ```tsx batchTransactions.tsx lines wrap expandable theme={null} async function checkCapabilities() { const provider = sdk.getProvider(); try { const cryptoAccount = await getCryptoKeyAccount(); const address = cryptoAccount?.account?.address; const capabilities = await provider.request({ method: "wallet_getCapabilities", params: [address], }); const baseCapabilities = capabilities[base.constants.CHAIN_IDS.base]; if (baseCapabilities?.atomicBatch?.supported) { console.log("Atomic batching is supported"); return true; } else { console.log("Atomic batching is not supported"); return false; } } catch (error) { console.error("Failed to check capabilities:", error); return false; } } ``` ### Non-Atomic Batching Sometimes you want calls to execute sequentially, even if some fail: ```tsx batchTransactions.tsx lines wrap expandable theme={null} const result = await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0.0", from: fromAddress, chainId: numberToHex(base.constants.CHAIN_IDS.base), atomicRequired: false, // Allow partial execution calls: calls, }, ], }); ``` ## Getting the Batch Transaction Result `wallet_getCallsStatus` returns the execution status for a batch you previously submitted with `wallet_sendCalls`. Capture the `callsId` returned by `wallet_sendCalls`, then poll for the batch status until it is confirmed or fails. ```tsx batchTransactions.tsx lines wrap expandable theme={null} async function trackBatchTransaction( calls: Array<{ to: `0x${string}`; data: `0x${string}`; value?: `0x${string}`; }> ) { const cryptoAccount = await getCryptoKeyAccount(); const fromAddress = cryptoAccount?.account?.address; const callsId = await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0.0", from: fromAddress, chainId: numberToHex(base.constants.CHAIN_IDS.base), atomicRequired: true, calls, }, ], }); try { const status = await provider.request({ method: "wallet_getCallsStatus", params: [callsId], }); if (status.status === 200) { console.log("Batch completed successfully", status.receipts); } else if (status.status === 100) { console.log("Batch still pending", status.id); } else { console.error("Batch failed", status.status); } return status; } catch (error: any) { if (error.code === 4200) { throw new Error("No batch found for the provided callsId."); } if (error.code === 4100) { throw new Error( "The connected wallet does not support wallet_getCallsStatus." ); } if (error.code === -32602) { throw new Error("The callsId parameter is invalid."); } throw error; } } ``` You can learn more about `wallet_getCallsStatus` in the [reference documentation](/sdks/base-account/reference/core/provider-rpc-methods/wallet_getCallsStatus).**Need more control over gas?** You can override the gas limit for individual calls in a batch using the [`gasLimitOverride`](/sdks/base-account/reference/core/capabilities/gasLimitOverride) capability. This is useful for calls with nondeterministic gas consumption, such as swaps. See the [capabilities overview](/sdks/base-account/reference/core/capabilities/overview) for the full list of supported capabilities. ## Video Guide # Use Spend Permissions Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/improve-ux/spend-permissions Learn how to use Spend Permissions to allow a trusted spender to spend user assets ## Overview Spend Permissions let you designate a trusted `spender` that can move assets out of a user's Base Account on their behalf. After the user signs the permission, the `spender` can initiate transfers within the limits you define — no additional prompts, pop-ups, or signatures needed from the user. This powers seamless experiences such as subscription renewals, algorithmic trading, and automated payouts. Read more about the Spend Permission Manager contract and supported chains on [GitHub](https://github.com/coinbase/spend-permissions).Spend Permissions for Base App Apps are coming soon and will be supported in a future update. If you're using Sub Accounts, learn how Base Account can automatically fund Sub Accounts and optionally skip approval prompts using [Auto Spend Permissions](/sdks/base-account/improve-ux/sub-accounts#auto-spend-permissions). ## Usage ### Request a Spend Permission You create an EIP-712 payload that describes the permission and ask the user to sign it. Store the resulting signature along with the permission data so you can register the permission on-chain later. The SDK helper below handles construction and signing for you. | Field Name | Type | Description | | ----------- | --------- | ---------------------------------------------------------------------------------------- | | `account` | `address` | Smart account this spend permission is valid for | | `spender` | `address` | Entity that can spend `account`'s tokens | | `token` | `address` | Token address (ERC-7528 native token or ERC-20 contract) | | `allowance` | `uint160` | Maximum allowed value to spend within each `period` | | `period` | `uint48` | Time duration for resetting used `allowance` on a recurring basis (seconds) | | `start` | `uint48` | Timestamp this spend permission is valid starting at (inclusive, unix seconds) | | `end` | `uint48` | Timestamp this spend permission is valid until (exclusive, unix seconds) | | `salt` | `uint256` | Arbitrary data to differentiate unique spend permissions with otherwise identical fields | | `extraData` | `bytes` | Arbitrary data to attach to a spend permission which may be consumed by the `spender` | ```tsx Request a Spend Permission lines wrap expandable theme={null} import { requestSpendPermission } from "@base-org/account/spend-permission"; import { createBaseAccountSDK } from "@base-org/account"; import { base } from "viem/chains"; const sdk = createBaseAccountSDK({ appName: 'Base Account SDK Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.id], }); const permission = await requestSpendPermission({ account: "0x...", spender: "0x...", token: "0x...", chainId: 8453, // or any other supported chain allowance: 1_000_000n, periodInDays: 30, provider: sdk.getProvider(), }); console.log("Spend Permission:", permission); ``` ### Use the Spend Permission Using a permission is 2 steps: 1. **Prepare the calls** — Call `prepareSpendCallData` with the permission and the requested `amount`. 2. **Submit the calls** — Submit the calls using your app's spender account. `prepareSpendCallData` returns an array of calls needed to spend the tokens: * `approveWithSignature` — When the permission is not yet registered onchain, this call would be prepended to the `spend` call. * `spend` — The call to spend the tokens from the user's Base Account. ```tsx Prepare Spend Call Data lines wrap expandable theme={null} import { prepareSpendCallData } from "@base-org/account/spend-permission"; // returns [approveWithSignatureCall, spendCall] const spendCalls = await prepareSpendCallData({ permission, amount, // optional; omit to spend the remaining allowance }); // If your app spender account supports wallet_sendCalls, submit them in batch using wallet_sendCalls // this is an example on how to do it using wallet_sendCalls in provider interface await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0", atomicRequired: true, from: spender, calls: spendCalls, }, ], }); // If your app spender account doesn't support wallet_sendCalls, submit them in order using eth_sendTransaction // this is an example on how to do it using eth_sendTransaction in provider interface await Promise.all( spendCalls.map((call) => provider.request({ method: "eth_sendTransaction", params: [ { ...call, from: spender, }, ], }) ) ); ```**About the `spendCalls` array** This array has 2 calls when submitting the permission onchain for *the first time*. When the permission is already registered onchain, this array has only 1 call (the `spend` call). For most use cases, you don't need to worry about this. ### Revoke a Spend Permission You can revoke a permission in two ways: * Request user approval via request to user's Base Account using `requestRevoke`. * Revoke silently from your app's spender by submitting the call returned from `prepareRevokeCallData`. ```tsx Revoke a Spend Permission lines wrap expandable theme={null} import { requestRevoke, prepareRevokeCallData, } from "@base-org/account/spend-permission"; // Option A: User-initiated revoke (wallet popup) try { const hash = await requestRevoke(permission); console.log("Revoke succeeded", hash); } catch { console.warn("Revoke was rejected or failed"); } // Option B: Silent revoke by your app's spender account const revokeCall = await prepareRevokeCallData(permission); // Submit the revoke call using your app's spender account // this is an example on how to do it using wallet_sendCalls in provider interface await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0", atomicRequired: true, from: spender, calls: [revokeCall], }, ], }); // If your app spender account doesn't support wallet_sendCalls, submit the revoke call using eth_sendTransaction // this is an example on how to do it using eth_sendTransaction in provider interface await provider.request({ method: "eth_sendTransaction", params: [ { ...revokeCall, from: spender, }, ], }); ``` ## API Reference * [requestSpendPermission](/sdks/base-account/reference/spend-permission-utilities/requestSpendPermission) * [prepareSpendCallData](/sdks/base-account/reference/spend-permission-utilities/prepareSpendCallData) * [requestRevoke](/sdks/base-account/reference/spend-permission-utilities/requestRevoke) * [prepareRevokeCallData](/sdks/base-account/reference/spend-permission-utilities/prepareRevokeCallData) * [fetchPermissions](/sdks/base-account/reference/spend-permission-utilities/fetchPermissions) * [fetchPermission](/sdks/base-account/reference/spend-permission-utilities/fetchPermission) * [getPermissionStatus](/sdks/base-account/reference/spend-permission-utilities/getPermissionStatus) ## Complete Integration Example ```typescript Complete Integration Example lines wrap expandable theme={null} import { fetchPermissions, fetchPermission, getPermissionStatus, prepareSpendCallData, requestSpendPermission, requestRevoke, prepareRevokeCallData, } from "@base-org/account/spend-permission"; import { createBaseAccountSDK } from "@base-org/account"; import { base } from "viem/chains"; const sdk = createBaseAccountSDK({ appName: 'Base Account SDK Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.id], }); const spender = "0xAppSpenderAddress"; // 1) Fetch a specific permission by its hash // Use fetchPermission when you already know the permission hash // (e.g., stored from a previous session or passed as a parameter) const permission = await fetchPermission({ permissionHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", provider: sdk.getProvider(), }); // Alternative: Fetch all permissions for a spender // Use fetchPermissions when you need to see all available permissions // and want to choose which one to use // const permissions = await fetchPermissions({ // account: "0xUserBaseAccountAddress", // chainId: 84532, // spender, // provider: sdk.getProvider(), // }); // const permission = permissions.at(0); // ======================================== // When there IS an existing permission // ======================================== // 2. check the status of permission try { const { isActive, remainingSpend } = await getPermissionStatus(permission); const amount = 1000n; if (!isActive || remainingSpend < amount) { throw new Error("No spend permission available"); } } catch { throw new Error("No spend permission available"); } // 3. prepare the calls const [approveCall, spendCall] = await prepareSpendCallData({ permission, amount, }); // 4. execute the calls using your app's spender account // this is an example using wallet_sendCalls, in production it could be using eth_sendTransaction. await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0", atomicRequired: true, from: spender, calls: [approveCall, spendCall], }, ], }); // ======================================== // When there is NOT an existing permission // ======================================== // 2. request a spend permission to use const newPermission = await requestSpendPermission({ account: "0xUserBaseAccountAddress", spender, token: "0xTokenContractAddress", chainId: 84532, allowance: 1_000_000n, periodInDays: 30, provider: sdk.getProvider(), }); // 3. prepare the calls const spendCalls = await prepareSpendCallData({ permission: newPermission, amount: 1_000n, }); // 4. execute the calls using your app's spender account // this is an example using eth_sendTransaction. If your app account supports wallet_sendCalls, use wallet_sendCalls to batch the calls instead. await Promise.all( spendCalls.map((call) => provider.request({ method: "eth_sendTransaction", params: [ { ...call, from: spender, }, ], }) ) ); // ======================================== // Request user to revoke spend permission // ======================================== try { const hash = await requestRevoke(permission); console.log("Revoke succeeded", hash); } catch { throw new Error("Revoke failed"); } // ======================================== // Revoke spend permission in the background // ======================================== const revokeCall = await prepareRevokeCallData(permission); await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0", atomicRequired: true, from: spender, calls: [revokeCall], }, ], }); ``` # Pay Gas in ERC20 Tokens Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/improve-ux/sponsor-gas/erc20-paymasters Base Account enables users to pay for gas in ERC20 tokens Base Account enables users to pay for gas in ERC20 tokens! Tokens can be accepted for payment by passed in app paymasters in addition to a set of universally supported tokens, such as USDC (this set to be expanded soon). This guide outlines how to set up your own app paymaster which will accept your token as payment. ## Choose a Paymaster Service Provider As a prerequisite, you'll need to obtain a paymaster service URL from a paymaster service provider. ERC20 paymasters have additional requirements that will be outlined below. We recommend the [Coinbase Developer Platform](https://www.coinbase.com/developer-platform) paymaster because it supports Base Account ERC20 token gas payments out of the box. CDP also provides free credits when you sign up. Otherwise if using a different paymaster provider, it must conform to the specification outlined in [ERC20 Compatible Paymasters](#erc20-compatible-paymasters) to correctly work with Base Account. ## App Setup for Custom Token Once you have a paymaster that is compatible with ERC20 gas payments on Base Account, you are only responsible for including the approvals to the paymaster for your token. It is recommended to periodically top up the allowance once they hit some threshold. ```js Top Up Paymaster Allowance lines wrap expandable theme={null} const tokenDecimals = 6 const minTokenThreshold = 1 * 10 ** tokenDecimals // $1 const tokenApprovalTopUp = 20 * 10 ** tokenDecimals // $20 const tokenAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" const nftContractAddress = "0x66519FCAee1Ed65bc9e0aCc25cCD900668D3eD49" const paymasterAddress = "0x2FAEB0760D4230Ef2aC21496Bb4F0b47D634FD4c" const mintTo = { abi: abi, functionName: "mintTo", to: nftContractAddress, args: [account.address, 1], }; calls = [mintTo] // Checks for allowance const allowance = await client.readContract({ abi: parseAbi(["function allowance(address owner, address spender) returns (uint256)"]), address: tokenAddress, functionName: "allowance", args: [account.address, paymasterAddress], }) if (allowance < minTokenThreshold) { // include approval for $20 in calls so that the paymaster will be able to move the token to accept payment calls.push({ abi: ["function approve(address,uint)"], functionName: "approve", to: nftContractAddress, args: [paymasterAddress, tokenApprovalTopUp], }) } ``` That is it! Base Account will handle the rest as long as it is compatible as outlined below. ## ERC20 Compatible Paymasters Coinbase Developer Platform is compatible out of the box and we will be working with other teams to include support soon! The paymaster must handle the `pm_getPaymasterStubData` and `pm_getPaymasterData` JSON-RPC requests specified by ERC-7677 in addition to `pm_getAcceptedPaymentTokens`. We step through each request and response below. ### pm\_getPaymasterStubData and pm\_getPaymasterData 1. The paymaster must use the specified ERC20 for payment if specified in the 7677 context field under `erc20`. 2. Upon rejection / failure the paymaster should return a `data` field in the JSONRPC response which could be used to approve the paymaster and includes: * `acceptedTokens` array which is a struct including the token address * `paymasterAddress` field which is the paymaster address which will perform the token transfers. 3. Upon success the paymaster must return a `tokenPayment` field in the result. This includes: * `tokenAddress` address of the token used for payment * `maxFee` the maximum fee to show in the UI * `decimals` decimals to use in the UI * `name` name of the token Base Account will simulate the transaction to ensure success and accurate information. #### Request This is a standard V0.6 Entrypoint request example with the additional context for the specified token to be used. ```json Request lines wrap expandable theme={null} { "jsonrpc": "2.0", "id": 1, "method": "pm_getPaymasterData", "params": [ { "sender": "0xe62B4aD6A7c079F47D77a9b939D5DC67A0dcdC2B", "nonce": "0x4e", "initCode": "0x", "callData": "0xb61d27f60000000000000000000000007746371e8df1d7099a84c20ed72e3335fb016b23000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", "callGasLimit": "0x113e10", "verificationGasLimit": "0x113e10", "preVerificationGas": "0x113e10", "maxFeePerGas": "0x113e10", "maxPriorityFeePerGas": "0x113e10", "paymasterAndData": "0x", "signature": "0x5ee079a5dec73fe39c1ce323955fb1158fc1b9a6b2ddbec104cd5cfec740fa5531584f098b0ca95331b6e316bd76091e3ab75a7bc17c12488664d27caf19197e1c" }, "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "0x2105", { "erc20": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" } ] } ``` #### Response Successful response: ```json Success Response lines wrap expandable theme={null} { "id": 1, "jsonrpc": "2.0", "result": { "paymasterAndData": "0x2faeb0760d4230ef2ac21496bb4f0b47d634fd4c0000670fdc98000000000000494b3b6e1d074fbca920212019837860000100833589fcd6edb6e08f4c7c32d4f71b54bda029137746371e8df1d7099a84c20ed72e3335fb016b23000000000000000000000000000000000000000000000000000000009b75458400000000697841102cd520d4e0171a58dadc3e6086111a49a90826cb0ad25579f25f1652081f68c17d8652387a33bf8880dc44ecf95be4213e786566d755baa6299f477b0bb21c", "tokenPayment": { "name": "USDC", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "maxFee": "0xa7c8", "decimals": 6 } } } ``` Rejection response: ```json Rejection Response lines wrap expandable theme={null} { "id": 1, "jsonrpc": "2.0", "error": { "code": -32002, "message": "request denied - no sponsorship and address can not pay with accepted token", "data": { "acceptedTokens": [ { "name": "USDC", "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ] } } } ``` ### pm\_getAcceptedPaymentTokens `pm_getAcceptedPaymentTokens` returns an array of tokens the paymaster will accept for payment. The request contains the entrypoint and the chain id with optional context. #### Request ```json Request theme={null} { "jsonrpc": "2.0", "id": 1, "method": "pm_getAcceptedPaymentTokens", "params": [ "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", "0x2105", {}] } ``` #### Response ```json Response lines wrap expandable theme={null} { "id": 1, "jsonrpc": "2.0", "result": { "acceptedTokens": [ { "name": "USDC", "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" } ] } } ``` # Sponsor Gas Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/improve-ux/sponsor-gas/paymasters Use Paymasters to sponsor your users' transactions One of the biggest UX enhancements unlocked by Base Account is the ability for app developers to sponsor their users' transactions. If your app supports Base Account, you can start sponsoring your users' transactions by using [standardized Paymaster service communication](https://erc7677.xyz) enabled by [new wallet RPC methods](https://eip5792.xyz). This guide is specific to using Base Account, you can find our more about using Paymasters with Base in the [Coinbase Developer Platform documentation](https://docs.cdp.coinbase.com/paymaster/introduction/welcome). ## Implementation Guide# Use Sub Accounts Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/improve-ux/sub-accounts Learn how to create and use Sub Accounts using Base Account SDK ## What Are Sub Accounts? Sub Accounts allow you to provision app-specific wallet accounts for your users that are embedded directly in your application. Once created, you can interact with them just as you would with any other wallet via the wallet provider or popular onchain libraries like wagmi and viem. As a prerequisite, you'll need to obtain a Paymaster service URL from a Paymaster service provider. We'll use [Coinbase Developer Platform](https://www.coinbase.com/developer-platform) as a Paymaster service provider. CDP provides free credits when you sign up. **ERC-7677-Compliant Paymaster Providers** If you choose to use a different Paymaster service provider, ensure they are [ERC-7677-compliant](https://www.erc7677.xyz/ecosystem/paymasters). Once you have signed up for [Coinbase Developer Platform](https://www.coinbase.com/developer-platform), you get your Paymaster service URL by navigating to **Onchain Tools > Paymaster** as shown below:How to get your Paymaster service URL
**Should you create a proxy for your Paymaster service?** We recommend using a proxy to protect the Paymaster service URL to prevent it from being exposed/leaked on a frontend client. For local development, you can use the same URL for the Paymaster service and the proxy. Once you have your Paymaster service URL, you can proceed to setting up your contracts allowlist. This is a list of contracts and function calls that you want to be sponsored by the Paymaster.Congrats! You've set up your Paymaster service and contracts allowlist. It's time to set up the Base Account SDK.How to set your Paymaster contracts allowlist
**You can also choose to create custom advanced policies !** You can create a `willSponsor` function to add some extra validation if you need more control over the policy enforcement. `willSponsor` is most likely not needed if you are using [Coinbase Developer Platform](https://www.coinbase.com/developer-platform) as it has built-in policy enforcement features, but know that this is still possible if you need it. Install and initialize the Base Account SDK to interact with Base Account: ### Installation ```bash npm theme={null} npm install @base-org/account ``` ```bash pnpm theme={null} pnpm add @base-org/account ``` ```bash yarn theme={null} yarn add @base-org/account ``` ```bash bun theme={null} bun add @base-org/account ``` ### Initialize the SDK ```tsx theme={null} import { createBaseAccountSDK, base } from '@base-org/account'; const sdk = createBaseAccountSDK({ appName: 'Paymaster Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.constants.CHAIN_IDS.baseSepolia], // or base.constants.CHAIN_IDS.base for mainnet }); const provider = sdk.getProvider(); ```Once you have your Paymaster service set up, you can now use `wallet_sendCalls` with paymaster capabilities to sponsor transactions. **Pass in the proxy URL** If you set up a proxy in your app's backend as recommended in step (1) above, you'll want to pass in the proxy URL you created. ### Basic Sponsored Transaction Here's how to send a sponsored transaction using the `wallet_sendCalls` RPC method: ```tsx theme={null} import { createBaseAccountSDK, getCryptoKeyAccount, base } from '@base-org/account'; import { numberToHex, encodeFunctionData, parseEther } from 'viem'; // Example NFT contract ABI const nftABI = [ { name: 'safeMint', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }], outputs: [] } ] as const; async function sendSponsoredTransaction() { const sdk = createBaseAccountSDK({ appName: 'Paymaster Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.constants.CHAIN_IDS.baseSepolia], }); const provider = sdk.getProvider(); try { // Get the user's account const cryptoAccount = await getCryptoKeyAccount(); const fromAddress = cryptoAccount?.account?.address; if (!fromAddress) { throw new Error('No account found'); } // Your Paymaster service URL (use your proxy URL) const paymasterServiceUrl = process.env.NEXT_PUBLIC_PAYMASTER_PROXY_SERVER_URL; // Prepare the transaction call const nftAddress = '0x119Ea671030FBf79AB93b436D2E20af6ea469a19'; const calls = [ { to: nftAddress, value: '0x0', data: encodeFunctionData({ abi: nftABI, functionName: 'safeMint', args: [fromAddress] }) } ]; // Send the transaction with paymaster capabilities const result = await provider.request({ method: 'wallet_sendCalls', params: [{ version: '1.0', chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia), from: fromAddress, calls: calls, capabilities: { paymasterService: { url: paymasterServiceUrl } } }] }); console.log('Sponsored transaction sent:', result); return result; } catch (error) { console.error('Sponsored transaction failed:', error); throw error; } } ``` ### Multiple Sponsored Transactions You can also batch multiple transactions and have them all sponsored: ```tsx theme={null} async function sendMultipleSponsoredTransactions() { const sdk = createBaseAccountSDK({ appName: 'Paymaster Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.constants.CHAIN_IDS.baseSepolia], }); const provider = sdk.getProvider(); const cryptoAccount = await getCryptoKeyAccount(); const fromAddress = cryptoAccount?.account?.address; const paymasterServiceUrl = process.env.NEXT_PUBLIC_PAYMASTER_PROXY_SERVER_URL; // Multiple calls in a single sponsored transaction const calls = [ { to: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', value: numberToHex(parseEther('0.001')), data: '0x' // Simple ETH transfer }, { to: '0x742d35Cc6634C0532925a3b844Bc9e7595f6E456', value: numberToHex(parseEther('0.001')), data: '0x' // Another ETH transfer } ]; const result = await provider.request({ method: 'wallet_sendCalls', params: [{ version: '1.0', chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia), from: fromAddress, calls: calls, capabilities: { paymasterService: { url: paymasterServiceUrl } } }] }); return result; } ``` ### Check Paymaster Capabilities Before sending sponsored transactions, you can check if the wallet supports paymaster services: ```tsx theme={null} async function checkPaymasterSupport() { const sdk = createBaseAccountSDK({ appName: 'Paymaster Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.constants.CHAIN_IDS.baseSepolia], }); const provider = sdk.getProvider(); const cryptoAccount = await getCryptoKeyAccount(); const address = cryptoAccount?.account?.address; try { const capabilities = await provider.request({ method: 'wallet_getCapabilities', params: [address] }); const baseCapabilities = capabilities[base.constants.CHAIN_IDS.baseSepolia]; if (baseCapabilities?.paymasterService?.supported) { console.log('Paymaster service is supported'); return true; } else { console.log('Paymaster service is not supported'); return false; } } catch (error) { console.error('Failed to check paymaster capabilities:', error); return false; } } ``` That's it! Base Account will handle the rest. If your Paymaster service is able to sponsor the transaction, in the UI Base Account will indicate to your user that the transaction is sponsored.Looking for a full implementation? Jump to the [Complete Integration Example](/sdks/base-account/improve-ux/sub-accounts#complete-integration-example). **Do you prefer video content?** There is a video guide that covers the implementation in detail in the [last section of this page](#video-guide). ## Key Benefits * **Frictionless transactions**: Eliminate repeated signing prompts for high frequency and agentic use cases or take full control of the transaction flow. * **No funding flows required**: Spend Permissions allow Sub Accounts to spend directly from the universal Base Account's balance. * **User control**: Users can manage all their sub accounts at [account.base.app](https://account.base.app).If you would like to see a live demo of Sub Accounts in action, check out our [Sub Accounts Demo](https://sub-accounts-fc.vercel.app). **Spend Permissions** Sub Accounts are optimized for use with Spend Permissions to allow your app to take advantage of the user's existing Base Account balances. See the [Spend Permissions](/sdks/base-account/improve-ux/spend-permissions) guide for more information about how they work. ## Installation Install the Base Account SDK:```bash npm theme={null} npm install @base-org/account ``` ```bash pnpm theme={null} pnpm add @base-org/account ``` ```bash yarn theme={null} yarn add @base-org/account ``` ```bash bun theme={null} bun add @base-org/account ``` ## Quickstart The fastest way to adopt Sub Accounts is to set `creation` to `on-connect` and `defaultAccount` to `sub` in the SDK configuration. ```tsx page.tsx theme={null} const sdk = createBaseAccountSDK({ // ... subAccounts: { creation: 'on-connect', defaultAccount: 'sub', } }); ``` This will automatically create a Sub Account for the user when they connect their Base Account and transactions will automatically be sent from the Sub Account unless you specify the `from` parameter in your transaction request to be the universal account address. Spend Permissions will also be automatically requested for the Sub Account as your app needs them. This is what the user will see when they connect their Base Account and automatic Sub Accounts are enabled:![]()
We recommend using a [Paymaster](/sdks/base-account/improve-ux/sponsor-gas/paymasters) to sponsor gas to ensure the best user experience when integrating Sub Accounts. You can set a paymaster to be used for all transactions by configuring the `paymasterUrls` parameter in the SDK configuration. See the [createBaseAccount](/sdks/base-account/reference/core/createBaseAccount#param-paymaster-urls) reference for more information. **Do you prefer video content?** There is a video guide that covers this specific implementation in the [last section of this page](#video-guide). ## Using Sub Accounts ### Initialize the SDK First, set up the Base Account SDK. Be sure to customize the `appName` and `appLogoUrl` to match your app as this will be displayed in the wallet connection popup and in the account.base.app dashboard. You can also customize the `appChainIds` to be the chains that your app supports. ```tsx page.tsx lines wrap expandable theme={null} import { createBaseAccountSDK, getCryptoKeyAccount } from '@base-org/account'; import { base } from 'viem/chains'; // Initialize SDK with Sub Account configuration const sdk = createBaseAccountSDK({ appName: 'Base Account SDK Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.id], }); // Get an EIP-1193 provider const provider = sdk.getProvider() ``` ### Create a Sub AccountMake sure to authenticate the user with their Base Account before creating a Sub Account. For that, you can choose one of the following options: * Follow the [Authenticate users](/sdks/base-account/guides/authenticate-users) guide * Simply use `provider.request({ method: 'eth_requestAccounts' });` for a simple wallet connection Create a Sub Account for your application using the provider's [wallet\_addSubAccount](/sdks/base-account/reference/core/provider-rpc-methods/wallet_addSubAccount) RPC method. When no `publicKey` parameter is provided, a non-extractable browser CryptoKey is generated and used to sign on behalf of the Sub Account. ```tsx page.tsx lines wrap expandable theme={null} // Create sub account const subAccount = await provider.request({ method: 'wallet_addSubAccount', params: [ { account: { type: 'create', }, } ], }); console.log('Sub Account created:', subAccount.address); ``` Alternatively, you can use the SDK convenience method: ```tsx page.tsx theme={null} const subAccount = await sdk.subAccount.create(); console.log('Sub Account created:', subAccount.address); ``` This is what the user will see when prompted to create a Sub Account:### Get Existing Sub Account Retrieve an existing Sub Account using the provider's [wallet\_getSubAccounts](/sdks/base-account/reference/core/provider-rpc-methods/wallet_getSubAccounts) RPC method. This will return the Sub Account associated with the app's domain and is useful to check if a Sub Account already exists for the user to determine if one needs to be created. ```tsx page.tsx lines wrap expandable theme={null} // Get the universal account const [universalAddress] = await provider.request({ method: "eth_requestAccounts", params: [] }) // Get sub account for universal account const { subAccounts: [subAccount] } = await provider.request({ method: 'wallet_getSubAccounts', params: [{ account: universalAddress, domain: window.location.origin, }] }) if (subAccount) { console.log('Sub Account found:', subAccount.address); } else { console.log('No Sub Account exists for this app'); } ``` Alternatively, you can use the SDK convenience method: ```tsx page.tsx theme={null} const subAccount = await sdk.subAccount.get(); console.log('Sub Account:', subAccount); ``` ### Send Transactions To send transactions from the connected sub account you can use EIP-5792 `wallet_sendCalls` or `eth_sendTransaction`. You need to specify the `from` parameter to be the sub account address.![]()
When the Sub Account is connected, it is the second account in the array returned by `eth_requestAccounts` or `eth_accounts`. `wallet_addSubAccount` needs to be called in each session before the Sub Account can be used. It will not trigger a new Sub Account creation if one already exists. If you are using `mode: 'auto'`, the Sub Account will be the first account in the array. First, get all the accounts that are available, of which the sub account will be the second account: ```tsx page.tsx theme={null} const [universalAddress, subAccountAddress] = await provider.request({ method: "eth_requestAccounts", // or "eth_accounts" if already connected params: [] }) ``` Then, send the transaction from the sub account: **`wallet_sendCalls`** ```tsx page.tsx lines wrap expandable theme={null} const callsId = await provider.request({ method: 'wallet_sendCalls', params: [{ version: "2.0", atomicRequired: true, from: subAccountAddress, // Specify the sub account address calls: [{ to: '0x...', data: '0x...', value: '0x...', }], capabilities: { // https://docs.cdp.coinbase.com/paymaster/introduction/welcome paymasterUrl: "https://...", }, }] }) console.log('Calls sent:', callsId); ``` **`eth_sendTransaction`** ```tsx page.tsx lines wrap expandable theme={null} const tx = await provider.request({ method: 'eth_sendTransaction', params: [{ from: subAccountAddress, // Specify the sub account address to: '0x...', data: '0x...', value: '0x...', }] }) console.log('Transaction sent:', tx); ``` We recommend using `wallet_sendCalls` in conjunction with a paymaster to ensure the best user experience. See the [Paymasters](/sdks/base-account/improve-ux/sponsor-gas/paymasters) guide for more information. ## Advanced Usage ### Import an Existing Account If you already have a deployed Smart Contract Account and would like to turn it into a Sub Account of the connected Base Account, you can import it as a Sub Account using the provider RPC method: ```tsx page.tsx lines wrap expandable theme={null} const subAccount = await provider.request({ method: 'wallet_addSubAccount', params: [ { account: { type: 'deployed', address: '0xYourSmartContractAccountAddress', chainId: 8453 // the chain the account is deployed on }, } ], }); console.log('Sub Account added:', subAccount.address); ```Before the Sub Account is imported, you will need to add the Base Account address as an owner of the Sub Account. This currently needs to be done manually by calling the [`addOwnerAddress`](https://github.com/coinbase/smart-wallet/blob/a8c6456f3a6d5d2dea08d6336b3be13395cacd42/src/MultiOwnable.sol#L101) or [`addOwnerPublicKey`](https://github.com/coinbase/smart-wallet/blob/a8c6456f3a6d5d2dea08d6336b3be13395cacd42/src/MultiOwnable.sol#L109) functions on the Smart Contract of the Sub Account that was imported and setting the Base Account address as the owner. Additionally, only Coinbase Smart Wallet contracts are currently supported for importing as a Sub Account into your Base Account. The Coinbase Smart Wallet contract ABI can be found on [GitHub](https://github.com/base/account-sdk/blob/master/packages/account-sdk/src/sign/base-account/utils/constants.ts#L8). ### Add Owner Account Sub Accounts automatically detect when an ownership update is needed when a signature is required and will prompt the user to approve the update before signing. However, you can also add an owner to a Sub Account manually using the SDK convenience method: ```tsx page.tsx theme={null} const ownerAccount = await sdk.subAccount.addOwner({ address: subAccount?.address, publicKey: cryptoAccount?.account?.publicKey, chainId: base.id, }); console.log('Owner added to Sub Account'); ``` This generates a transaction to call the `addOwnerAddress` or `addOwnerPublicKey` functions on the Sub Account's smart contract to add the owner.Ownership changes are expected if the user signs in to your app on a new device or browser. Ensure you do not lose your app's Sub Account signer keys when using the SDK on the server (e.g. Node.js) as updating the owner requires a signature from the user, which cannot be requested from server contexts. ## Auto Spend Permissions Auto Spend Permissions allows Sub Accounts to access funds from their parent Base Account when transaction balances are insufficient. This feature can also establish ongoing spend permissions, enabling future transactions to execute without user approval prompts, reducing friction in your app's transaction flow. This feature is **enabled by default** when using Sub Accounts. ### How It Works **First-time transaction flow:** When a Sub Account attempts its first transaction, Base Account displays a popup for user approval. During this approval process, Base Account: * Automatically detects any missing tokens (native or ERC-20) needed for the transaction * Requests a transfer of the required funds from the parent Base Account to the Sub Account to fulfill the current transaction * Allows the user to optionally grant ongoing spend permissions for future transactions in that token **Subsequent transactions:** If the user granted spend permissions, future transactions follow this priority: 1. First, attempt using existing Sub Account balances and granted spend permissions 2. If insufficient, prompt the user to authorize additional transfers and/or spend permissions from their Base AccountSpend permission requests are limited to the first token when multiple transfers are needed for a single transaction. Additional tokens require separate approvals. ### Configuration If your users' Sub Accounts will be funded manually, you can disable Auto Spend Permissions by setting `funding` to `manual` in your SDK configuration: ```tsx page.tsx lines wrap expandable theme={null} const sdk = createBaseAccountSDK({ appName: 'Base Account SDK Demo', appLogoUrl: 'https://base.org/logo.png', appChainIds: [base.id], subAccounts: { funding: 'manual', // Disable auto spend permissions } }); ``` ## Technical Details Base Account's self-custodial design requires a user passkey prompt for each wallet interaction, such as transactions or message signing. While this ensures user awareness and approval of every wallet interaction, it can impact user experience in applications requiring frequent wallet interactions. To support Base Account with user experiences that need more developer control over wallet interactions, we've built Sub Accounts in conjunction with [ERC-7895](https://eip.tools/eip/7895), a new wallet RPC for creating hierarchical relationships between wallet accounts. These Sub Accounts are linked to the end user's Base Account through an onchain relationship. When combined with our [Spend Permission feature](/sdks/base-account/improve-ux/spend-permissions), this creates a powerful foundation for provisioning and funding app accounts securely, while giving you ample control over building the user experience that makes the most sense for your application. ## Complete Integration Example Here's a full React component that demonstrates Sub Account creation and usage: ```tsx page.tsx lines wrap expandable theme={null} import { createBaseAccountSDK } from "@base-org/account"; import { useCallback, useEffect, useState } from "react"; import { baseSepolia } from "viem/chains"; interface SubAccount { address: `0x${string}`; factory?: `0x${string}`; factoryData?: `0x${string}`; } interface GetSubAccountsResponse { subAccounts: SubAccount[]; } interface WalletAddSubAccountResponse { address: `0x${string}`; factory?: `0x${string}`; factoryData?: `0x${string}`; } export default function SubAccountDemo() { const [provider, setProvider] = useState["getProvider"] > | null>(null); const [subAccount, setSubAccount] = useState (null); const [universalAddress, setUniversalAddress] = useState (""); const [connected, setConnected] = useState(false); const [loadingSubAccount, setLoadingSubAccount] = useState(false); const [loadingUniversal, setLoadingUniversal] = useState(false); const [status, setStatus] = useState(""); // Initialize SDK and crypto account useEffect(() => { const initializeSDK = async () => { try { const sdkInstance = createBaseAccountSDK({ appName: "Sub Account Demo", appChainIds: [baseSepolia.id], }); // Get the provider const providerInstance = sdkInstance.getProvider(); setProvider(providerInstance); setStatus("SDK initialized - ready to connect"); } catch (error) { console.error("SDK initialization failed:", error); setStatus("SDK initialization failed"); } }; initializeSDK(); }, []); const connectWallet = async () => { if (!provider) { setStatus("Provider not initialized"); return; } setLoadingSubAccount(true); setStatus("Connecting wallet..."); try { // Connect to the wallet const accounts = (await provider.request({ method: "eth_requestAccounts", params: [], })) as string[]; const universalAddr = accounts[0]; setUniversalAddress(universalAddr); setConnected(true); // Check for existing sub account const response = (await provider.request({ method: "wallet_getSubAccounts", params: [ { account: universalAddr, domain: window.location.origin, }, ], })) as GetSubAccountsResponse; const existing = response.subAccounts[0]; if (existing) { setSubAccount(existing); setStatus("Connected! Existing Sub Account found"); } else { setStatus("Connected! No existing Sub Account found"); } } catch (error) { console.error("Connection failed:", error); setStatus("Connection failed"); } finally { setLoadingSubAccount(false); } }; const createSubAccount = async () => { if (!provider) { setStatus("Provider not initialized"); return; } setLoadingSubAccount(true); setStatus("Creating Sub Account..."); try { const newSubAccount = (await provider.request({ method: "wallet_addSubAccount", params: [ { account: { type: 'create', }, } ], })) as WalletAddSubAccountResponse; setSubAccount(newSubAccount); setStatus("Sub Account created successfully!"); } catch (error) { console.error("Sub Account creation failed:", error); setStatus("Sub Account creation failed"); } finally { setLoadingSubAccount(false); } }; const sendCalls = useCallback( async ( calls: Array<{ to: string; data: string; value: string }>, from: string, setLoadingState: (loading: boolean) => void ) => { if (!provider) { setStatus("Provider not available"); return; } setLoadingState(true); setStatus("Sending calls..."); try { const callsId = (await provider.request({ method: "wallet_sendCalls", params: [ { version: "2.0", atomicRequired: true, chainId: `0x${baseSepolia.id.toString(16)}`, // Convert to hex from, calls, capabilities: { // https://docs.cdp.coinbase.com/paymaster/introduction/welcome // paymasterUrl: "your paymaster url", }, }, ], })) as string; setStatus(`Calls sent! Calls ID: ${callsId}`); } catch (error) { console.error("Send calls failed:", error); setStatus("Send calls failed"); } finally { setLoadingState(false); } }, [provider] ); const sendCallsFromSubAccount = useCallback(async () => { if (!subAccount) { setStatus("Sub account not available"); return; } const calls = [ { to: "0x4bbfd120d9f352a0bed7a014bd67913a2007a878", data: "0x9846cd9e", // yoink value: "0x0", }, ]; await sendCalls(calls, subAccount.address, setLoadingSubAccount); }, [sendCalls, subAccount]); const sendCallsFromUniversal = useCallback(async () => { if (!universalAddress) { setStatus("Universal account not available"); return; } const calls = [ { to: "0x4bbfd120d9f352a0bed7a014bd67913a2007a878", data: "0x9846cd9e", // yoink value: "0x0", }, ]; await sendCalls(calls, universalAddress, setLoadingUniversal); }, [sendCalls, universalAddress]); return ( ); } ``` ## Video Guide # Telemetry · Base Account Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/telemetry Understanding Base Account's anonymous telemetry system and how to configure it. Base Account includes an anonymous telemetry system to help us better understand how Base Account is used across applications and improve the developer experience. Participation in this anonymous program is optional—if you'd prefer not to share any usage data, you can easily opt out. ## Why Are We Collecting Telemetry? Base Account provides critical wallet infrastructure for onchain applications with features like signing transactions & messages, signer management, and more. By collecting telemetry data, we can: * **Monitor Wallet Operation Success**: Track which wallet operations (signing, connecting, transactions) are most reliable and identify failure patterns * **Data-Informed Improvements**: Help our engineering team generate insights that drive future wallet enhancements and reliability improvements * **Proactive Issue Detection**: Quickly detect issues with new SDK releases or wallet operations through operational metrics and error monitoring ## What Data Will Be Collected? Telemetry data is completely anonymous and focused on functional metrics. Specifically, we collect: * **Request Success Metrics**: Information about the success and failure rates of wallet requests to identify reliability issues * **Error Events**: Generic error events with operational context to help us improve Base Account reliability * **UI Component Usage**: Anonymous metrics on interface component functionality to ensure optimal reliability **Privacy First**: No sensitive data—such as private keys, transaction contents, user addresses, or personal information—is ever collected. ## How Does It Work? Telemetry is integrated into the Base Account SDK and automatically triggers when certain wallet events occur (provided telemetry is enabled in your configuration). The data is sent to secure Coinbase endpoints for analysis. For example, when a wallet request occurs, a telemetry event like this is sent: ```bash Example Telemetry Event theme={null} curl 'https://cca-lite.coinbase.com/amp' \ -H 'content-type: application/x-www-form-urlencoded; charset=utf-8' \ -H 'origin: https://your-app.com' \ --data-raw 'e=[{"event_type":"scw_signer.request.started","event_properties":{"method":"eth_requestAccounts","correlationId":"abc123-def456","sdkVersion":"4.3.2","appName":"Your App Name","appOrigin":"https://your-app.com"}}]' ``` The payload contains an array of telemetry events with operational data including: * **event\_type**: The specific wallet operation being tracked * **method**: The wallet method being called (e.g., `eth_requestAccounts`) * **correlationId**: A unique identifier for request tracking * **sdkVersion**: The Base Account SDK version * **appName**: Your application name * **appOrigin**: Your application's domain ## How Do I Opt Out? By default, telemetry collection follows an opt-out model. If you'd like to disable telemetry in your app that uses the Base Account SDK, you can configure it during SDK initialization: ```typescript Disable Telemetry lines wrap expandable theme={null} import { createBaseAccountSDK } from "@base-org/account"; const sdk = createBaseAccountSDK({ appName: "My App", appLogoUrl: "https://example.com/logo.png", preference: { telemetry: false, // [!code focus] }, }); const provider = sdk.getProvider(); ``` For legacy Coinbase Wallet SDK class based components: ```tsx Legacy SDK Opt Out lines wrap expandable theme={null} import { CoinbaseWalletSDK } from "@coinbase/wallet-sdk"; const sdk = new CoinbaseWalletSDK({ appName: "My App", appLogoUrl: "https://example.com/logo.png", }); const provider = sdk.getProvider({ telemetry: false, // [!code focus] }); ``` We believe that this telemetry initiative will help us make Base Account even better for all developers—by focusing our improvements on the most critical wallet operations and catching issues early. If you have any questions or feedback, please reach out to the Base Account team. Happy building with Base Account! — The Base Account team # Gas Usage Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/troubleshooting/usage-details/gas-usage Why Base Accounts use more gas than traditional Ethereum accounts and what it costs users on L2. Base Accounts use more gas for transactions than traditional Ethereum accounts. On L2 networks, the cost difference to the user is a matter of cents. The gas difference is due to the additional overhead required for: 1. **ERC-4337 Bundling** 2. **Smart Contract Operations**, including one time deployment of the Base Account contract 3. **Signature Verification** ## Gas Usage Breakdown Here's a rough comparison of gas usage per account: | Operation Type | Traditional Ethereum Account | Base Account | | --------------------- | ---------------------------- | -------------- | | Native Token Transfer | \~21,000 gas | \~100,000 gas | | ERC-20 Token Transfer | \~65,000 gas | \~150,000 gas | | First Deployment | N/A | \~300,000+ gas | # Popup Tips Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/troubleshooting/usage-details/popups Troubleshoot Base Account popup behavior and resolve common popup issues. ## Overview When a Base Account is connected and Coinbase Wallet SDK receives a request, it opens [keys.coinbase.com](https://keys.coinbase.com/) in a popup window and passes the request to the popup for handling. Keep the following points in mind when working with the Base Account popup. ## Default Blocking Behavior * Most modern browsers block all popups by default, unless they are triggered by a click. * If a popup is blocked the browser shows a notification to the user, allowing them to manage popup settings. ### What to Do About It * Ensure there is no additional logic between the button click and the request to open the Base Account popup, as browsers might perceive the request as programmatically initiated. * If logic is unavoidable, keep it minimal and test thoroughly in all supported browsers. ## `Cross-Origin-Opener-Policy` If the Base Account popup opens and displays an error or infinite spinner, it may be due to the dapp's `Cross-Origin-Opener-Policy`. Be sure to use a directive that allows the Base Account popup to function. * ✅ Allows Base Account popup to function * `unsafe-none` (default) * `same-origin-allow-popups` (recommended) * ❌ Breaks Base Account popup * `same-origin` For more detailed information refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy). ## Base Account Popup 'Linger' Behavior * Sometimes a dapp may programmatically make a followup request based on the response to a previous request. Normally, browsers block these programmatic requests to open popups. * To address this, after the Base Account popup responds to a request, it will linger for 200ms to listen for another incoming request before closing. * If a request is received *during* this 200ms window, it will be received and handled within the same popup window. * If a request is received *after* the 200ms window and the popup has closed, opening the Base Account popup will be blocked by the browser. # Transaction Simulation Data Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/troubleshooting/usage-details/simulations Copy Base Account transaction simulation data to inspect requests and responses while debugging. There is a hidden feature which enables you to easily copy transaction simulation request and response data which can then be pasted it in a text editor to inspect. ## Instructions * Click the area defined in red ***five times***, then paste the copied data in a text editor.Sub Account Demo
Status: {status}
{universalAddress && (Universal Account: {universalAddress}
)} {subAccount && (Sub Account: {subAccount.address}
)}{!connected ? ( ) : !subAccount ? ( ) : ()}# Unsupported Calls Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/troubleshooting/usage-details/unsupported-calls RPC calls that Base Account does not support under EIP-1193 and ERC-4337, and what to use instead. Base Account implements an Ethereum Provider JavaScript API (as detailed in [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193)) and follows the [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) standard for account abstraction. This means that apps interacting with Base Account can expect it to behave like a regular Ethereum Virtual Machine (EVM) account. However, there are some calls that are not supported by Base Accounts for incompatibility or security reasons. This is a list of calls that are not supported: * [Self Calls](#self-calls): This refers to the ability of an app to use the user's account to call itself. * [CREATE](#create): This refers to the ability of an app to create a new contract using the OPCODE `CREATE`. ## Self Calls For security reasons, we do not allow 3rd party apps to make calls to a user's own Base Account address. This could allow apps to change owners, upgrade the user's account, or cause other issues. ## CREATE This is a limitation of the ERC-4337 standard and smart accounts. Currently, we do not support the `CREATE` opcode for smart contracts deployment. Future versions of Base Account may support it.![]()
You can use a factory contract or a transaction with the `CREATE2` opcode to deploy a smart contract. ## Solidity's Builtin `transfer` Function The `transfer` function is a built-in member of the `address` type in Solidity that can be used to send ETH to an address. Base Account wallets cannot receive ETH using this function. This function has long been considered deprecated in favor of `call` by the Solidity community, but some older contracts still use it. The reason for this is that `transfer` only forwards 2300 gas to the `transfer` call, a protective mechanism that was designed to prevent reentrancy attacks by limiting the amount of gas available to a smart contract that might reenter the caller. In the modern world of smart contract wallets (including for Base Account), this is often not enough gas for the smart contract's `receive` or `fallback` functions to complete their work, causing the transaction to revert. ### Known Affected Contracts * The [WETH9 contract](https://basescan.org/token/0x4200000000000000000000000000000000000006) uses `transfer` to send ETH to the user's wallet and therefore Base Accounts cannot directly unwrap ETH from it. # Wallet Library Support Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/more/troubleshooting/usage-details/wallet-library-support Popular wallet libraries and their support status for Base Account. Below are some popular wallet libraries and what we know of their plans for day 1 support for Base Account. | Name | Support | | -------------------------------------------------------------------------------------------- | ------- | | [Dynamic](https://www.dynamic.xyz/docs/react/wallets/external-wallets/coinbase-smart-wallet) | ✅ | | [Privy](https://docs.privy.io/guide/react/recipes/misc/coinbase-smart-wallets) | ✅ | | [ThirdWeb](http://portal.thirdweb.com/connect) | ✅ | | [ConnectKit](https://docs.family.co/connectkit) | ✅ | | [Web3Modal](https://docs.reown.com/web3modal/react/smart-accounts) | ✅ | | [Web3-Onboard](https://www.blocknative.com/coinbase-wallet-integration) | ✅ | | [RainbowKit](https://www.rainbowkit.com/) | ✅ | # Base Account SDK Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/overview Add universal sign-in and one-tap USDC payments to any app with the Base Account SDK — the onchain account layer powering the Base App. The Base Account SDK connects your app to the onchain accounts that power the [Base App](https://base.app) — over one hundred thousand users with a passkey-backed [Smart Wallet](/sdks/base-account/reference/onchain-contracts/smart-wallet). Add sign-in and USDC payments in a few lines; users hold their own keys and you never touch private data or funds.## Install One passkey works across every Base-enabled app — no installs, seed phrases, or network switches. A single `pay()` call handles gas and USDC settlement. Prove verified account ownership and add Sybil-resistant identity checks. Create and charge USDC subscriptions with the Base Account SDK. ```bash npm theme={null} npm install @base-org/account ``` ```bash pnpm theme={null} pnpm add @base-org/account ``` ```bash yarn theme={null} yarn add @base-org/account ``` ## Quickstart## Explore Call `pay()` with an amount and a recipient to collect USDC. ```typescript theme={null} import { pay } from '@base-org/account'; const payment = await pay({ amount: "5.00", to: "0xRecipient" }); console.log(`Sent — transaction ID: ${payment.id}`); ``` Use Sign in with Base to authenticate with a passkey — no password, no email round-trip. Wire up Sign in with Base in your web or React app. Pick a framework or explore the full API surface below. # AI Tools for Base Account Developers Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/quickstart/ai-tools-available-for-devs AI tools available to Base Account developers, including MCP servers and prompt resources. Base Account has a number of AI tools available for builders and developers. We keep expanding the list of tools and features, so please check back soon for updates. Build and run in five minutes on web, React, or mobile. Drop into Wagmi, Privy, RainbowKit, Reown, or thirdweb. Every method — `pay`, `getPaymentStatus`, subscriptions, and charges. The EIP-1193 provider surface exposed by the SDK. ## Base Builder MCP This repository is an [Model Context Protocol](https://modelcontextprotocol.io/introduction) server destined for Base Builders. It contains a list of tools that you can give your AI coding assistant to help it build with Base Account In particular, it allows your AI coding assistant to efficiently find the right guides that are relevant to the code you are writing. [Base Builder MCP](https://github.com/base/base-builder-mcp) ## llms.txt File This is a simple text file that contains the full context of our documentation for your LLMs. It is a convenient and useful tool for your AI coding assistant to help it build with Base Account. [LLMs.txt File](/llms-full.txt) ## Agent Kit This is a tool that allows you to build your AI agent using embedded Wallet APIs. It is a great starting point for your AI agent projects with Base Account. [Agent Kit](https://docs.cdp.coinbase.com/agentkit/docs/welcome) # Mobile (React Native) Source: https://base-a060aa97-roethke-update-hf-details.mintlify.site/sdks/base-account/quickstart/mobile-integration Integrate Base Account into a React Native mobile app with sign-in and USDC payments. This guide helps you add support for Base Account into a React Native app by integrating the [Mobile Wallet Protocol Client](https://www.npmjs.com/package/@mobile-wallet-protocol/client). } /> llms-txt } />} /> This doc is updated for Mobile Wallet Protocol Client `v1.0.0` **Deep Link Handling** Breaking change in v1.0.0: Universal Links and App Links requirements are removed in favor of custom schemes (e.g. `myapp://`). ## Before You Start This guide walks you through adding support for Base Account into an existing React Native app or starter project. If you prefer to skip ahead and start with a working example, navigate to the repository below:If you are looking to integrate Base Account into an existing React Native app or starter project, follow the instructions below. ## Step 1: Install Mobile Wallet Protocol Client Add the latest version of [Mobile Wallet Protocol Client](https://mobilewalletprotocol.github.io/wallet-mobile-sdk/) to your project. ```zsh npm theme={null} npm i @mobile-wallet-protocol/client@latest ``` ```zsh yarn theme={null} yarn add @mobile-wallet-protocol/client@latest ``` ## Step 2: Add Polyfills ### Install Peer Dependencies The Mobile Wallet Protocol Client library requires the [Expo WebBrowser](https://docs.expo.dev/versions/latest/sdk/webbrowser/) and [Async Storage](https://react-native-async-storage.github.io/2.0/Installation/) packages to be installed. Follow the instructions on the respective pages for any additional setup.```zsh npm theme={null} npm i expo expo-web-browser @react-native-async-storage/async-storage ``` ```zsh yarn theme={null} yarn add expo expo-web-browser @react-native-async-storage/async-storage ``` ### Polyfills Mobile Wallet Protocol Client requires `crypto.randomUUID`, `crypto.getRandomValues`, and `URL` to be polyfilled globally since they are not available in the React Native environment. Below is an example of how to polyfill these functions in your app using the [expo-crypto](https://docs.expo.dev/versions/latest/sdk/crypto/) and [expo-standard-web-crypto](https://github.com/expo/expo/tree/master/packages/expo-standard-web-crypto/) packages.```zsh npm theme={null} npm i expo-crypto expo-standard-web-crypto react-native-url-polyfill ``` ```zsh yarn theme={null} yarn add expo-crypto expo-standard-web-crypto react-native-url-polyfill ``` ```js polyfills.js theme={null} import "react-native-url-polyfill/auto"; import { polyfillWebCrypto } from "expo-standard-web-crypto"; import { randomUUID } from "expo-crypto"; polyfillWebCrypto(); crypto.randomUUID = randomUUID; ``` ```tsx App.tsx theme={null} import "./polyfills"; // import before @mobile-wallet-protocol/client import { CoinbaseWalletSDK } from "@mobile-wallet-protocol/client"; /// ... ``` ## Step 3: Usage Mobile Wallet Protocol Client provides 2 interfaces for mobile app to interact with the Base Account, an EIP-1193 compliant provider interface and a wagmi connector.If your app is using wallet aggregator, go straight to [**Option 2: Wagmi Connector**](#option-2-wagmi-connector) for 1-line integration. ### Option 1: EIP-1193 ProviderThe `app` prefix in SDK config params is removed in v1.0.0. Create a new `EIP1193Provider` instance, which is EIP-1193 compliant. ```tsx App.tsx lines wrap expandable theme={null} import { EIP1193Provider } from "@mobile-wallet-protocol/client"; // Step 1. Initialize provider with your dapp's metadata and target wallet const metadata = { name: "My App Name", customScheme: "myapp://", // only custom scheme (e.g. `myapp://`) is supported in v1.0.0 chainIds: [8453], logoUrl: "https://example.com/logo.png", }; const provider = new EIP1193Provider({ metadata, wallet: Wallets.CoinbaseSmartWallet, }); // ... // 2. Use the provider const addresses = await provider.request({ method: "eth_requestAccounts" }); const signedData = await provider.request({ method: "personal_sign", params: ["0x48656c6c6f20776f726c6421", addresses[0]], }); ``` ### Option 2: wagmi Connector Add the latest version of Mobile Wallet Protocol wagmi-connectors to your project.```zsh npm theme={null} npm i @mobile-wallet-protocol/wagmi-connectors@latest ``` ```zsh yarn theme={null} yarn add @mobile-wallet-protocol/wagmi-connectors@latest ``` Simply import the `createConnectorFromWallet` function and pass in the wallet you want to use to wagmi config. ```ts config.ts lines wrap expandable theme={null} import { createConnectorFromWallet, Wallets, } from "@mobile-wallet-protocol/wagmi-connectors"; const metadata = { name: "My App Name", customScheme: "myapp://", // only custom scheme (e.g. `myapp://`) is supported in v1.0.0 chainIds: [8453], logoUrl: "https://example.com/logo.png", }; export const config = createConfig({ chains: [base], connectors: [ createConnectorFromWallet({ metadata, wallet: Wallets.CoinbaseSmartWallet, }), ], transports: { [base.id]: http(), }, }); ``` Then you can use wagmi's react interface to interact with the Base Account. ```tsx App.tsx lines wrap expandable theme={null} import { useConnect } from "wagmi"; // ... const { connect, connectors } = useConnect(); return (