diff --git a/.changeset/purple-ducks-scream.md b/.changeset/purple-ducks-scream.md
new file mode 100644
index 000000000..a09736628
--- /dev/null
+++ b/.changeset/purple-ducks-scream.md
@@ -0,0 +1,5 @@
+---
+'@solana/react': minor
+---
+
+Add a `useAirdrop` hook that wraps a client's `airdrop` capability (`ClientWithAirdrop`) as a tracked `useAction`. `dispatch(address, amount)` requests an airdrop with an injected `AbortSignal`, resolving with the transaction `Signature` (or `undefined` when the airdrop is applied without a transaction).
diff --git a/packages/react/README.md b/packages/react/README.md
index 47ab888e2..b4697ec34 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -169,6 +169,27 @@ function SendButton({ client, instructions }) {
- **`usePlanTransaction(client)`** / **`usePlanTransactions(client)`** — plan a single, or multiple, transaction message(s) from an instruction input. Requires a client with transaction planning installed (`ClientWithTransactionPlanning`). `usePlanTransaction` resolves with a single transaction message; `usePlanTransactions` resolves with the full transaction plan.
- **`useSendTransaction(client)`** / **`useSendTransactions(client)`** — sign, submit, and confirm a single, or multiple, transaction(s). Requires a client with transaction sending installed (`ClientWithTransactionSending`). Both accept flexible input (instructions, an instruction plan, or a transaction plan); `useSendTransaction` additionally accepts a single transaction message.
+### `useAirdrop(client)`
+
+Request an airdrop of SOL to an address as a tracked action. Requires a client with an airdrop plugin installed (`ClientWithAirdrop`) — typically available on devnet, testnet, and local validators. Returns the same `ActionResult` as [`useAction`](#useactionfn); `dispatch(address, amount)` supplies the recipient and lamport amount while the hook injects the `AbortSignal`. A natural fit for a devnet "fund this account" button.
+
+```tsx
+import { useAirdrop } from '@solana/react';
+import { lamports } from '@solana/kit';
+import type { Address } from '@solana/kit';
+
+function AirdropButton({ client, address }: { client: ClientWithAirdrop; address: Address }) {
+ const { dispatch, isRunning } = useAirdrop(client);
+ return (
+
+ );
+}
+```
+
+`data` resolves to the transaction `Signature`, or `undefined` when the airdrop was applied without a transaction (some implementations, e.g. LiteSVM, adjust balances directly) — null-check it before use.
+
### Payer & identity
Read the signer a client uses to pay for transactions (`payer`), or the wallet whose on-chain assets the app acts upon (`identity`), and re-render whenever it changes. Both hooks return the current `TransactionSigner`, or `undefined` while none is available.
diff --git a/packages/react/src/__tests__/useAirdrop-test.browser.tsx b/packages/react/src/__tests__/useAirdrop-test.browser.tsx
new file mode 100644
index 000000000..d98a14002
--- /dev/null
+++ b/packages/react/src/__tests__/useAirdrop-test.browser.tsx
@@ -0,0 +1,62 @@
+import type { Address, ClientWithAirdrop, Lamports, Signature } from '@solana/kit';
+import { act } from '@testing-library/react';
+
+import { renderHook } from '../__test-utils__/render';
+import { useAirdrop } from '../useAirdrop';
+
+describe('useAirdrop', () => {
+ const address = 'AACC' as Address;
+ const amount = 1_000_000_000n as Lamports;
+ const signature = 'sig' as Signature;
+
+ it('calls client.airdrop with the address, amount, and an abort signal, then resolves', async () => {
+ expect.assertions(5);
+ const { promise, resolve } = Promise.withResolvers();
+ const airdrop = jest.fn(() => promise);
+ const client = { airdrop } as unknown as ClientWithAirdrop;
+ const { result } = renderHook(() => useAirdrop(client));
+
+ expect(result.current.status).toBe('idle');
+
+ act(() => {
+ result.current.dispatch(address, amount);
+ });
+ expect(result.current.status).toBe('running');
+ expect(airdrop).toHaveBeenCalledWith(address, amount, expect.any(AbortSignal));
+
+ await act(async () => resolve(signature));
+ expect(result.current.status).toBe('success');
+ expect(result.current.data).toBe(signature);
+ });
+
+ it('resolves with `undefined` when the airdrop was performed without a transaction', async () => {
+ expect.assertions(2);
+ const { promise, resolve } = Promise.withResolvers();
+ const airdrop = jest.fn(() => promise);
+ const client = { airdrop } as unknown as ClientWithAirdrop;
+ const { result } = renderHook(() => useAirdrop(client));
+
+ act(() => {
+ result.current.dispatch(address, amount);
+ });
+ await act(async () => resolve(undefined));
+ expect(result.current.status).toBe('success');
+ expect(result.current.data).toBeUndefined();
+ });
+
+ it('surfaces a rejection as an error', async () => {
+ expect.assertions(2);
+ const boom = new Error('boom');
+ const { promise, reject } = Promise.withResolvers();
+ const airdrop = jest.fn(() => promise);
+ const client = { airdrop } as unknown as ClientWithAirdrop;
+ const { result } = renderHook(() => useAirdrop(client));
+
+ act(() => {
+ result.current.dispatch(address, amount);
+ });
+ await act(async () => reject(boom));
+ expect(result.current.status).toBe('error');
+ expect(result.current.error).toBe(boom);
+ });
+});
diff --git a/packages/react/src/__typetests__/useAirdrop-typetest.ts b/packages/react/src/__typetests__/useAirdrop-typetest.ts
new file mode 100644
index 000000000..8dc764c56
--- /dev/null
+++ b/packages/react/src/__typetests__/useAirdrop-typetest.ts
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/no-floating-promises */
+/* eslint-disable react-hooks/rules-of-hooks */
+
+import type { Address, ClientWithAirdrop, ClientWithRpc, Lamports, Signature } from '@solana/kit';
+
+import type { ActionResult } from '../useAction';
+import { useAirdrop } from '../useAirdrop';
+
+// [DESCRIBE] useAirdrop
+{
+ const client = {} as ClientWithAirdrop;
+ const address = {} as Address;
+ const amount = {} as Lamports;
+
+ // It returns an ActionResult over the (address, amount) args and the optional signature
+ {
+ const result = useAirdrop(client);
+ result satisfies ActionResult<[address: Address, amount: Lamports], Signature | undefined>;
+ result.dispatch(address, amount) satisfies void;
+ result.dispatchAsync(address, amount) satisfies Promise;
+ result.data satisfies Signature | undefined;
+ }
+
+ // dispatch rejects a non-Address first argument
+ {
+ const { dispatch } = useAirdrop(client);
+ // @ts-expect-error - first argument must be an Address
+ dispatch(123, amount);
+ }
+
+ // dispatch rejects a non-Lamports second argument
+ {
+ const { dispatch } = useAirdrop(client);
+ // @ts-expect-error - second argument must be Lamports
+ dispatch(address, 123);
+ }
+
+ // dispatch rejects a missing amount argument
+ {
+ const { dispatch } = useAirdrop(client);
+ // @ts-expect-error - amount is required
+ dispatch(address);
+ }
+
+ // It rejects a client that lacks an airdrop capability
+ {
+ const rpcOnlyClient = {} as ClientWithRpc;
+ // @ts-expect-error - client must have an airdrop capability
+ useAirdrop(rpcOnlyClient);
+ }
+}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 45c4889a4..93935b15c 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -5,6 +5,7 @@
*/
export * from './ClientProvider';
export * from './useAction';
+export * from './useAirdrop';
export * from './useClient';
export * from './useClientCapability';
export * from './useIdentity';
diff --git a/packages/react/src/useAirdrop.ts b/packages/react/src/useAirdrop.ts
new file mode 100644
index 000000000..3c87cd11b
--- /dev/null
+++ b/packages/react/src/useAirdrop.ts
@@ -0,0 +1,45 @@
+import type { Address, ClientWithAirdrop, Lamports, Signature } from '@solana/kit';
+
+import { type ActionResult, useAction } from './useAction';
+
+/**
+ * Requests an airdrop of SOL to an address, as a reactive action.
+ *
+ * Wraps `client.airdrop` with {@link useAction}: each `dispatch(address, amount)` runs the airdrop
+ * with a fresh `AbortSignal` and tracks its lifecycle through React state. Calling `dispatch` again
+ * while a previous airdrop is in flight aborts the first. This is a great fit for a devnet or
+ * localnet "fund this account" button, where the `isRunning` / `data` / `error` tracking drives the
+ * UI directly.
+ *
+ * The airdrop capability is typically available on test networks (devnet, testnet) and local
+ * validators. Some implementations (e.g. LiteSVM) update balances directly without sending a
+ * transaction, in which case the resolved `data` is `undefined` rather than a {@link Signature}.
+ *
+ * @param client - A client with an airdrop plugin installed (`ClientWithAirdrop`).
+ * @returns An {@link ActionResult} whose `dispatch`/`dispatchAsync` take the recipient `address` and
+ * the `amount` of lamports, and resolve with the transaction {@link Signature}, or `undefined`
+ * when the airdrop was performed without a transaction.
+ *
+ * @example
+ * ```tsx
+ * import { useAirdrop } from '@solana/react';
+ * import { lamports } from '@solana/kit';
+ *
+ * function AirdropButton({ client, address }) {
+ * const { dispatch, isRunning } = useAirdrop(client);
+ * return (
+ *
+ * );
+ * }
+ * ```
+ *
+ * @see {@link ActionResult}
+ * @see {@link useAction}
+ */
+export function useAirdrop(
+ client: ClientWithAirdrop,
+): ActionResult<[address: Address, amount: Lamports], Signature | undefined> {
+ return useAction((abortSignal, address: Address, amount: Lamports) => client.airdrop(address, amount, abortSignal));
+}