---
name: push-backend
description: "Use when writing Node.js scripts, bots, or server-side code with @pushchain/core - covers PushChain.initialize, wrapping ethers/viem/Solana keypairs into a UniversalSigner, all three routes, multichain cascades, transaction tracking, Universal Read (read, prepareRead, executeReads, trackRead: EVM, Solana and Web2 state delivered on-chain to Push Chain), and utility functions. Not for browser or React code. Triggers on: 'initialize PushChain client in Node.js', 'send transaction from backend script', 'wrap ethers signer with toUniversal', 'track transaction by hash', 'read a balance or contract value from another chain onto Push Chain'."
metadata:
  id: push-backend
  intent: 'Execute universal transactions from server-side code, scripts, bots, and automation'
  package: '@pushchain/core'
  package_version: '6.0.26'
  current_sdk_version: '6.0.26'
  entry: 'PushChain.initialize'
  resources: 'https://push.org/agents/resources/push-backend/index.json'
  references: 'references/signer-options.md, references/initialize-client.md, references/send-universal-transaction.md, ../../workflows/send-multichain-transaction.md, ../../workflows/universal-read.md'
---

# Skill: Universal Transactions - Backend (Node.js / Scripts)

**Intent**: Execute universal transactions from server-side code, scripts, bots, and automation pipelines.
**Package**: `@pushchain/core` - no other library (ethers.js, viem, wagmi) can replace `sendTransaction`, `signMessage`, `prepareTransaction`, or `executeTransactions`.

> **Full agent layer:** [push.org/llms.txt](https://push.org/llms.txt) indexes every skill, workflow, example, error code, constant, and routing decision in the Push Chain agent layer. Pull it when this skill points outside its domain — cross-skill context, unknown progress-hook IDs, error recovery, or constants lookups.

> **PUSD stablecoin?** For minting, redeeming, or integrating **PUSD** (par-backed) and **PUSD+** (yield-bearing) — both native on Push Chain Donut — see the dedicated [push-pusd skill](https://pusd.push.org/agents/skill/push-pusd/SKILL.md) (or [pusd.push.org/llms.txt](https://pusd.push.org/llms.txt) for the full PUSD agent-layer index: ABIs, deployment addresses, examples). Covers Node.js mint/redeem flows, the multicall sentinel pattern for one-signature deposits, and the on-chain Solidity interfaces for protocols holding PUSD/PUSD+.

## Install

```bash
npm install @pushchain/core
```

## Push Chain - EVM Compatible

Push Chain is **100% EVM-compatible**. Any ethers.js or viem code that targets Ethereum works on Push Chain by pointing at the Push Chain RPC - same API, same tooling, no changes needed.

|                    | Value                             |
| ------------------ | --------------------------------- |
| **HTTP RPC**       | `https://evm.donut.rpc.push.org/` |
| **WebSocket RPC**  | `wss://evm.donut.rpc.push.org`    |
| **Chain ID**       | `42101`                           |
| **Block Explorer** | `https://donut.push.network`      |

> For **read-only** queries (transactions, blocks, balances, contract view calls) - no SDK needed. Use `ethers.JsonRpcProvider` or `viem.createPublicClient` directly. See [Read Blockchain State](#read-blockchain-state).
>
> For **sending transactions** - use `@pushchain/core` (`PushChain.initialize` + `sendTransaction`). Standard ethers/viem cannot cross chains or route through UEAs.

---

## Read Blockchain State

For read-only queries - transactions, blocks, balances, contract view calls, WebSocket subscriptions - no Push Chain SDK needed. Use standard ethers or viem pointed at the Push Chain RPC above.

### Initialize (ethers)

```ts
import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/');
const tx = await provider.getTransaction('0x...');
const block = await provider.getBlock('latest');
const bal = await provider.getBalance('0xAddress');
```

### Initialize (viem)

```ts
import { createPublicClient, http } from 'viem';

const client = createPublicClient({
  transport: http('https://evm.donut.rpc.push.org/'),
});
const block = await client.getBlock();
const bal = await client.getBalance({ address: '0xAddress' });
```

### WebSocket (real-time block subscription)

```ts
// ethers
const ws = new ethers.WebSocketProvider('wss://evm.donut.rpc.push.org');
ws.on('block', (n) => console.log('New block:', n));

// viem
import { createPublicClient, webSocket } from 'viem';
const wsClient = createPublicClient({
  transport: webSocket('wss://evm.donut.rpc.push.org'),
});
const stop = wsClient.watchBlocks({
  onBlock: (b) => console.log('New block:', b.number),
});
```

Full reference: https://push.org/agents/workflows/read-blockchain-state.md

---

## Universal Origin - Send from Any Chain

Universal transactions can originate from **any supported chain** - Push Chain, Ethereum, Solana, BNB, Arbitrum, Base, or any supported chain. The user's wallet stays on their home chain; Push Chain routes execution transparently.

**The primary pattern for all EVM chains** (ethers.js or viem) is identical - the **RPC URL determines the origin chain**, not a separate parameter:

```ts
import { PushChain } from '@pushchain/core';
import { ethers } from 'ethers';

// Origin chain = Ethereum Sepolia (Sepolia RPC)
const provider = new ethers.JsonRpcProvider(
  'https://sepolia.gateway.tenderly.co'
);
// Origin chain = Push Chain (Donut RPC)
// const provider = new ethers.JsonRpcProvider('https://evm.donut.rpc.push.org/');
// Origin chain = BNB Testnet (BNB RPC)
// const provider = new ethers.JsonRpcProvider('https://data-seed-prebsc-1-s1.binance.org:8545/');

// Never hardcode - load from env; never log the key
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const universalSigner = await PushChain.utils.signer.toUniversal(wallet);
```

For Solana, explicitly pass the chain and library:

```ts
import { Keypair } from '@solana/web3.js';

// Never hardcode - load from env; never log the key or the raw buffer
const solKeypair = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.SOLANA_KEY!))
);
const universalSigner = await PushChain.utils.signer.toUniversalFromKeypair(
  solKeypair,
  {
    chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET,
    library: PushChain.CONSTANTS.LIBRARY.SOLANA_WEB3JS, // Available LIBRARY values: SOLANA_WEB3JS (more may be added - see https://push.org/agents/workflows/constants-reference.md)
  }
);
```

After creating any signer, call `PushChain.initialize` to get the client:

```ts
const client = await PushChain.initialize(universalSigner, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET, // optional, defaults to TESTNET
});
// Returns PushChainClient - use client.universal.* for all operations
```

> **Frontend / React?** Use `@pushchain/ui-kit` instead - `PushUniversalWalletProvider` handles signer creation and `PushChain.initialize` automatically. Access the ready client via `usePushChainClient()`. See [push-frontend skill](https://push.org/agents/skills/push-frontend/SKILL.md).

**Quick reference (backend / scripts):**

Always two steps - create a chain-native signer, then convert it to a `UniversalSigner`:

| Origin                     | Step 1 - Create native signer                                             | Step 2 - Create Universal Signer                                             |
| -------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Ethereum / EVM (ethers.js) | `new ethers.Wallet(key, provider)` ← RPC picks chain                      | `PushChain.utils.signer.toUniversal(ethersSigner)`                           |
| Ethereum / EVM (viem)      | `createWalletClient({ account, transport: http(rpc) })` ← RPC picks chain | `PushChain.utils.signer.toUniversal(viemClient)`                             |
| Solana                     | `Keypair.fromSecretKey(...)`                                              | `PushChain.utils.signer.toUniversalFromKeypair(keypair, { chain, library })` |
| Custom / any               | implement `signMessage`, `signAndSendTransaction`, `signTypedData`        | `PushChain.utils.signer.construct(account, signingMethods)`                  |

> **Atomic batching capability (EIP-7702).** `toUniversal` also wires an optional `signAuthorization` method for ethers v6 `Wallet` and viem **local** accounts - this is what lets a Push-native EOA run [multicall](#multicall---batch-multiple-calls) atomically in one EIP-7702 tx. JSON-RPC/browser accounts and ethers v5 can't sign 7702 authorizations, so multicall falls back to sequential execution. Custom signers can pass `signAuthorization` in `construct` to opt in - see [signer-options reference](references/signer-options.md).

See all supported chains: `PushChain.utils.chains.getSupportedChainsByName(PushChain.CONSTANTS.PUSH_NETWORK.TESTNET)`

## Initialize Client

`PushChain.initialize(signerOrAccount, options?)` → `Promise<PushChainClient>`

| Argument          | Type                                  | Default   | Description                                                             |
| ----------------- | ------------------------------------- | --------- | ----------------------------------------------------------------------- |
| `signerOrAccount` | `UniversalSigner \| UniversalAccount` | -         | `UniversalSigner` for full write/sign; `UniversalAccount` for read-only |
| `options.network` | `PushChain.CONSTANTS.PUSH_NETWORK`    | `TESTNET` | Push Chain network to connect to                                        |
| `options.rpcUrls` | `Partial<Record<CHAIN, string[]>>`    | `{}`      | Custom RPC URLs per chain                                               |

**Full write/sign** (pass a `UniversalSigner`):

```ts
const client = await PushChain.initialize(universalSigner, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});
```

> ⚠️ Passing a `UniversalAccount` creates a **read-only client**. Calling `sendTransaction`, `signMessage`, `prepareTransaction`, or `executeTransactions` will throw. Use `UniversalSigner` for any write operation.

**Read-only** - pass a `UniversalAccount` (no private key). `universal.account`, `universal.origin`, and `explorer.*` still work:

```ts
const account = PushChain.utils.account.toUniversal('0xAddress', {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
});
const client = await PushChain.initialize(account, {
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});
```

**Reinitialize** - swap signer/account or update options without creating a fresh instance:

```ts
const newClient = await client.reinitialize(newSignerOrAccount, {
  // override options as needed (rpcUrls, blockExplorers, etc.)
});
// reinitialize always returns a new client - update your reference
```

**Access account info** after initialization:

```ts
client.universal.origin;  // { address: string, chain: CHAIN } — source chain wallet (object)
client.universal.account; // `0x${string}` — the Push Chain execution account address (UEA for
                          // cross-chain users, native EOA for Push-native users). Plain string,
                          // NOT an object.
```

> ⚠️ **Shape difference.** `origin` is an object (`{ address, chain }`); `account` is a **plain address string** — no `.address` field. Reading `client.universal.account.address` returns `undefined` and silently breaks dependent guards. Use `client.universal.account` directly.

**Verify initialization succeeded:**

```ts
console.log('origin:', client.universal.origin);   // { address, chain } - object, matches your signer
console.log('account:', client.universal.account); // '0x...' string - UEA (cross-chain) or EOA (Push-native)
```

**Account status** - UEA deployment and version (SDK handles upgrades automatically in most cases):

```ts
const status = await client.getAccountStatus();
// { mode: 'signer' | 'read-only', uea: { deployed, version, minRequiredVersion, requiresUpgrade } }
```

> **UI Kit (frontend):** `PushChain.initialize` is called automatically by `PushUniversalWalletProvider`. Use `usePushChainClient()` to access the ready client - no manual initialization needed.

> `client.orchestrator` is reserved for internal SDK use (RPC resolution, UEA management, gas orchestration) - do not call methods on it directly.

> **Reading blockchain state** does NOT require `@pushchain/core`. Use ethers.js or viem directly with the Push Chain RPC `https://evm.donut.rpc.push.org/`. The `PushChainClient` is for **sending and signing universal transactions** - not general-purpose EVM reads.

## Send Universal Transaction

`client.universal.sendTransaction(tx)` → `Promise<TxResponse>`

### Routing - determined by `tx.to` and `tx.from`

| Route   | `tx.to`                 | `tx.from`   | Executes on    | Via                           |
| ------- | ----------------------- | ----------- | -------------- | ----------------------------- |
| Route 1 | `'0x...'` plain address | omitted     | Push Chain     | UEA                           |
| Route 2 | `{ address, chain }`    | omitted     | External chain | CEA on target chain           |
| Route 3 | `'0x...'` plain address | `{ chain }` | Push Chain     | CEA on `from.chain` as origin |

### Arguments

| Argument                  | Type                                                                                            | Description                                                                                                                                                                                            |
| ------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| _`tx.to`_                 | `string \| { address: string; chain: CHAIN }`                                                   | Execution target. Plain address → Route 1. `{ address, chain }` → Route 2.                                                                                                                             |
| `tx.from`                 | `{ chain: CHAIN }` _(optional)_                                                                 | Forces CEA on the specified external chain as execution origin → Route 3.                                                                                                                              |
| `tx.value`                | `bigint` _(optional)_                                                                           | Native value in smallest unit - uPC on Push Chain; native asset on external routes.                                                                                                                    |
| `tx.data`                 | `string \| Array<{ to: string; value: bigint; data: string }>` _(optional)_                     | Encoded calldata for a single call or multicall array. EVM: `encodeTxData({ abi, functionName, args })`. Solana: `encodeTxData({ idl, functionName, args })`. Multicall requires `tx.to: '0x000...0'`. |
| `tx.funds`                | `{ amount: bigint; token?: MOVEABLE.TOKEN \| { chain: CHAIN; address: string } }` _(optional)_  | Move supported assets as part of the tx. Two token shapes: a `MoveableToken` accessor for external-born tokens (PRC-20 path), or a `{ chain, address }` **PC-20 reference** for Push-born tokens — see [Moving Tokens: PRC-20 vs PC-20](#moving-tokens-with-txfunds---prc-20-vs-pc-20). For Route 1: external origin only (Push-native users use ERC-20 `transfer` directly).                             |
| `tx.progressHook`         | `(progress: ProgressHookType) => void` _(optional)_                                             | Callback for per-step lifecycle events. Event IDs are route-prefixed (`SEND-TX-1xx` Route 1, `SEND-TX-2xx` Route 2, `SEND-TX-3xx` Route 3). See [ProgressHook Events](#progresshook-events) below.     |
| `tx.payGasWith`           | `{ token?: PAYABLE.TOKEN; slippageBps?: number; minAmountOut?: bigint \| string }` _(optional)_ | Pay universal gas fees with a supported ERC-20 instead of native.                                                                                                                                      |
| `tx.gasLimit`             | `bigint` _(optional, SDK estimated)_                                                            | Override gas limit.                                                                                                                                                                                    |
| `tx.maxFeePerGas`         | `bigint` _(optional, SDK estimated)_                                                            | Override max fee per gas.                                                                                                                                                                              |
| `tx.maxPriorityFeePerGas` | `bigint` _(optional, SDK estimated)_                                                            | Override priority fee.                                                                                                                                                                                 |
| `tx.deadline`             | `bigint` _(optional)_                                                                           | Execution deadline.                                                                                                                                                                                    |
| `tx.options.enforceGasCheck` | `boolean` _(optional, defaults to `false`)_                                                  | Pre-flight gas / balance check mode. `false` (default): emit a `WARNING` progress event on shortfall and proceed (the SDK's fee-locking / refill paths usually recover). `true`: emit an `ERROR` progress event and throw [`InsufficientUEABalanceError`](#insufficient_uea_balance) before broadcast. Use `true` when you want pre-flight guarantees over best-effort retries. Same option is accepted by `prepareTransaction`; setting it on any single hop opts the entire cascade into strict mode.                                          |

---

### ProgressHook Events

Each `progress` object passed to `tx.progressHook` has this shape:

```ts
{
  id: string; // e.g. 'SEND-TX-101'
  title: string;
  message: string;
  level: 'INFO' | 'SUCCESS' | 'ERROR';
  response: object | null; // extra data: chain, address, txHash, etc.
  timestamp: string; // ISO-8601
}
```

Event IDs are prefixed by route. Key milestones per route:

| Route                    | Prefix            | First event   | Success          | Failure          |
| ------------------------ | ----------------- | ------------- | ---------------- | ---------------- |
| Route 1 → Push Chain     | `SEND-TX-1xx`     | `SEND-TX-101` | `SEND-TX-199-01` | `SEND-TX-199-02` |
| Route 2 → External chain | `SEND-TX-2xx`     | `SEND-TX-201` | `SEND-TX-299-01` | `SEND-TX-299-02` |
| Route 3 CEA → Push Chain | `SEND-TX-3xx`     | `SEND-TX-301` | `SEND-TX-399-01` | `SEND-TX-399-02` |
| Multichain cascade       | `SEND-TX-0xx/9xx` | `SEND-TX-001` | `SEND-TX-999-01` | `SEND-TX-999-02` |

Full event list (all routes, all IDs, response shapes): https://push.org/agents/workflows/progress-hook-events.md

---

### Route 1 - Any Origin → Push Chain (via UEA)

`tx.to` is a plain address. The user's UEA executes the call on Push Chain.

```ts
const tx = await client.universal.sendTransaction({
  to: '0xContractOnPushChain',
  value: PushChain.utils.helpers.parseUnits('0.01', 18),
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
});
const receipt = await tx.wait();
if (receipt.status !== 1) throw new Error(`tx failed: ${receipt.hash}`);
console.log('status:', receipt.status === 1 ? 'success' : 'failed');
```

> `funds` is only needed on Route 1 when the **origin is an external chain** and you want to move assets to Push Chain atomically. Push-native users call ERC-20 `transfer` directly instead.

---

### Route 2 - Any Origin → External Chain (via CEA)

`tx.to` is `{ address, chain }`. Push Chain coordinates execution through the user's CEA on the target chain.

```ts
const tx = await client.universal.sendTransaction({
  to: {
    address: '0xContractOnBnb',
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  },
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
  funds: {
    amount: PushChain.utils.helpers.parseUnits('1', 6),
    token: PushChain.CONSTANTS.MOVEABLE.TOKEN.BNB_TESTNET.USDT,
  },
});
const receipt = await tx.wait();
console.log('external hash:', receipt.externalTxHash);
console.log('explorer:', receipt.externalExplorerUrl);
```

> Route 2 receipt includes additional fields: `externalTxHash`, `externalChain`, `externalExplorerUrl`.

**Solana target variant** - same Route 2 shape; pass the Anchor IDL via `encodeTxData` and the SDK resolves all accounts, PDAs, and the CEA automatically:

```ts
import testCounterIdl from './target/idl/test_counter.json';

const data = PushChain.utils.helpers.encodeTxData({
  idl: testCounterIdl, // Anchor IDL from target/idl/*.json
  functionName: 'receive_sol', // snake_case or camelCase both accepted
  args: [BigInt(0)], // use BigInt for u64/u128 args
});
const tx = await client.universal.sendTransaction({
  to: {
    address: '8yNqjrMnFiFbVTVQcKij8tNWWTMdFkrDf9abCGgc2sgx',
    chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET,
  },
  value: BigInt(0),
  data,
});
const receipt = await tx.wait();
console.log('Solana tx hash:', receipt.externalTxHash);
console.log('Solana explorer:', receipt.externalExplorerUrl);
```

---

### Route 3 - CEA Origin → Push Chain

Add `from: { chain }` to use your CEA on an external chain as the execution origin on Push Chain. `msg.sender` inside the target contract will be the CEA, not the UEA.

**Why CEAs exist.** When your Push Chain account first interacts with an external chain (e.g. calling Aave on Ethereum), the protocol deterministically deploys a **Chain Executor Account (CEA)** for you on that chain. This CEA:

1. **Preserves identity** - your actions on Ethereum are traceable to a stable, deterministic address derived from your Push Chain account.
2. **Isolates risk** - the CEA is a dedicated smart account, separate from your home wallet. External-chain actions can't affect funds outside the CEA.
3. **Enables payload execution** - the CEA is what actually holds assets and executes calldata on the external chain.

**When to use Route 3.** Use it when you need to bring state or assets _back_ to Push Chain from a CEA you've already deployed - because only the CEA can speak for what happened on that external chain.

Example flow - a universal vault:

1. Route 2: vault calls Aave on Ethereum via your **Ethereum CEA** to withdraw USDC.
2. **Route 3**: the vault moves the withdrawn USDC from your Ethereum CEA back to Push Chain (`from: { chain: ETHEREUM_SEPOLIA }`) - `msg.sender` on Push Chain is your Ethereum CEA, which holds the tokens.
3. Route 2 again: Push Chain forwards to a Solana lending protocol via your **Solana CEA**.

```ts
// Bring assets/state back from the Ethereum CEA to Push Chain
const tx = await client.universal.sendTransaction({
  from: { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }, // your Ethereum CEA as origin
  to: '0xVaultOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'depositFromEthereum',
  }),
});
await tx.wait();
// Inside 0xVaultOnPushChain: msg.sender === CEA(Ethereum Sepolia, userAddress)
// The CEA is the only address that can prove "these assets came from your Ethereum side".
```

```
User → Push vault
  Route 2 → Ethereum CEA → Aave (withdraw USDC)
  Route 3 ← Ethereum CEA → Push vault (receive USDC, msg.sender = Ethereum CEA)
  Route 2 → Solana CEA → Solana lending (deposit)
```

> Route 3 isn't for new outbound flows - use Route 2 for those. Route 3 is the return path from a CEA you've already deployed via prior Route 2 activity.

**Route 3 funding pattern (what the dev actually has to send).** Depends on whether an asset is moving off the external chain:

| Sub-pattern | Fund | Why |
|-------------|------|-----|
| Plain contract call / multicall on Push Chain (no `funds`, no `value` on the call) | Source-chain UOA only (e.g. ~0.01 Sepolia ETH) | The SDK fee-locks source-chain native, mints PC into the UEA, then UEA → UGPC swaps PC into the destination-chain native to cover CEA gas + first-time CEA deployment. CEA does not need a manual top-up. |
| Bridge native back (`from: { chain }` + `value` + `to: client.universal.account`) | UOA on source chain AND the native asset on the CEA on the named external chain (amount = `value`) | The native asset being swept back has to physically sit on the CEA before the call. CEA gas still comes from the source-chain fee-lock. |
| Bridge funds back (`from: { chain }` + `funds: { amount, token: MOVEABLE.<chain>.<TOKEN> }` + `to: client.universal.account`) | UOA on source chain AND the SDK-registered PRC-20 source token on the CEA on the named external chain (amount = `funds.amount`) | ERC-20 variant of the above. Mint the SDK-registered token at `PushChain.CONSTANTS.MOVEABLE.TOKEN.<CHAIN>.<TOKEN>.address` (e.g. `0xE935d9c9C24D02E61186c640cc01d713C876d40F` for USDT on BNB Testnet) and transfer to the printed CEA address. |
| Funds-with-payload (bridge an asset back AND atomically call a Push Chain contract) | Same as bridge-funds-back | The payload runs on Push Chain via the UEA after the bridge settles. |

> Anti-pattern: telling devs to send `0.02 BNB / SOL` to the CEA "for gas + fee-lock deposit" for a plain Route 3 call. The CEA gas is automatic; only ask for the CEA-side asset when an asset is actually being bridged back. Even then, ask for the burn amount itself (e.g. `0.001 BNB`, `0.01 USDT`), not a 10x buffer.

**Solana CEA as origin** - set `from: { chain: SOLANA_DEVNET }` to use the user's Solana CEA as the origin. `msg.sender` on Push Chain will be the Solana CEA address. Use `deriveExecutorAccount` beforehand if you need to fund it.

```ts
// Route 3 - Solana CEA origin → Push Chain contract
const tx = await client.universal.sendTransaction({
  from: { chain: PushChain.CONSTANTS.CHAIN.SOLANA_DEVNET }, // Solana CEA as origin
  to: '0xContractOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi,
    functionName: 'increment',
  }),
});
await tx.wait();
// Inside 0xContractOnPushChain: msg.sender === CEA(Solana Devnet, userAddress)
```

---

### Moving Tokens with `tx.funds` - PRC-20 vs PC-20

`funds.token` accepts two shapes. The SDK tells them apart by the **absence of `symbol`** - a `{ chain, address }` object with no `symbol` field is a PC-20 reference; adding `symbol` makes the SDK treat it as a `MoveableToken` and misroute it.

| Token was born on…                                   | Shape to pass                                                                     | Lookup                                                     | What happens                                                                                          |
| ---------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| An **external chain** (USDT on Sepolia, SOL, …)      | `MoveableToken` accessor - `PushChain.CONSTANTS.MOVEABLE.TOKEN.<CHAIN>.<TOKEN>`   | `getPRC20Address` - sync, static SDK table                 | Origin token is locked; its synthetic **PRC-20** (`USDC.eth`, `pETH`) is minted on Push Chain          |
| **Push Chain** (any standard ERC-20 deployed there)  | **PC-20 reference** - `{ chain, address }`, nothing else                          | `getPC20Address` - async, UniversalCore on-chain registry  | Canonical token is locked in VaultPC20; a **wrapper** deploys on the destination chain on first export |

> PRC-20 = a foreign token wearing a Push jacket. PC-20 = a Push token wearing a foreign jacket. One letter apart, opposite directions - canonical definitions: [Token Types on Push Chain](https://push.org/docs/chain/important-concepts/#token-types-on-push-chain). Any ERC-20 born on Push Chain with standard `name()` / `symbol()` / `decimals()` is a PC-20 - there is no interface to implement.

**PC-20 export - Push-born token to an external chain:**

```ts
// 1. Resolve the token first. getPC20Address reports its decimals, so the
//    amount is right by construction. It accepts either end of the mapping
//    (canonical Push address or an external wrapper) and always returns the
//    canonical token, with every confirmed deployment in .registry.
const token = await PushChain.utils.tokens.getPC20Address('0xTokenBornOnPush', {
  chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT, // where the address lives
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});

// 2. Send. funds.token.chain = where the tokens sit RIGHT NOW (Push Chain);
//    to.chain = the destination. A mismatch throws PC20TokenChainMismatchError
//    before anything broadcasts.
const tx = await client.universal.sendTransaction({
  to: {
    address: '0xRecipientOnSepolia',
    chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
  },
  funds: {
    amount: PushChain.utils.helpers.parseUnits('1', token.decimals),
    token: {
      chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT,
      address: token.address,
      // no symbol - ever. Its absence is what marks this as a PC-20 reference.
    },
  },
});
await tx.wait();

// 3. Read the wrapper address from the registry - the authoritative record.
//    receipt.externalAssetAddr is a best-effort mirror: the raw chain field is
//    only observed when a wrapper is newly deployed, and although the SDK
//    backfills it from UniversalCore it is undefined while the outbound is
//    still in flight.
const resolved = await PushChain.utils.tokens.getPC20Address(token.address, {
  chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT,
  network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
});
const wrapper = resolved.registry.find((e) => e.chainName === 'ETHEREUM_SEPOLIA');
if (!wrapper) throw new Error('Wrapper not registered yet - retry in a moment.');
```

Bringing a wrapper home is the same call from the external side: `funds.token = { chain: <external chain>, address: <wrapper> }` with `to` on Push Chain. The gateway's PC20Factory burns via `burnFrom`, so the return leg sends **no approval transaction**.

Every PC-20 failure is a typed error extending `PC20Error`, with a stable `code` (`PC20_TOKEN_CHAIN_MISMATCH`, `PC20_WRAPPER_NOT_REGISTERED`, `PC20_EXPECTED_BUT_PRC20`, `PC20_AMBIGUOUS_ADDRESS`, …), curated context fields, and a remediation hint. Classify by `instanceof PC20Error` or `err.code`, never by parsing messages - full catalog with recovery steps: [errors.json](https://push.org/agents/errors.json).

> Supplying a synthetic PRC-20 address (e.g. `USDC.eth`) as a PC-20 reference throws `PC20_EXPECTED_BUT_PRC20` - a PRC-20 is a legitimate token on the wrong API, not a broken one. Move it with its `MoveableToken` accessor instead.

---

### Multicall - Batch Multiple Calls

Pass an array to `tx.data` and set `tx.to` to the zero address. The zero-address target signals multicall mode to the SDK; individual call targets are in the `data` array.

Multicall works from **any origin**:

- **External-chain origin** (Ethereum, Solana, BNB, ...) - the batch executes atomically through the UEA (unchanged behavior).
- **Push-native EOA origin** - the batch executes atomically in a **single EIP-7702 type-4 transaction** via the PushBatchExecutor when the signer can sign EIP-7702 authorizations. `PushChain.utils.signer.toUniversal` wires this automatically for ethers v6 `Wallet` and viem **local** accounts (e.g. `privateKeyToAccount`). If the signer can't sign an authorization (JSON-RPC/browser wallets, ethers v5), the SDK logs a warning and safely falls back to sequential per-call execution - **non-atomic**: an earlier call stays committed if a later one reverts.

Check `atomic` on the response to know which path ran (`true` = single atomic tx; `false` = sequential fallback).

> **Zero address rule.** Only the **outer** `tx.to` is the zero address (that's what signals multicall mode). Every **inner** entry's `to` must be an explicit non-zero target - a zero inner `to` is rejected with a `PushChainExecutionError` (the EIP-7702 executor reinterprets a zero target as the account itself).

```ts
const tx = await client.universal.sendTransaction({
  to: '0x0000000000000000000000000000000000000000', // required for multicall (outer target only)
  data: [
    { to: '0xContract1', value: 0n, data: call1Data }, // inner targets must be non-zero
    { to: '0xContract2', value: 0n, data: call2Data },
  ],
});
console.log('atomic:', tx.atomic); // false = Push-native sequential fallback ran
```

---

### TxResponse

| Field                  | Type                                     | Description                                |
| ---------------------- | ---------------------------------------- | ------------------------------------------ |
| `hash`                 | `string`                                 | Push Chain transaction hash                |
| `origin`               | `string`                                 | CAIP-10 origin: `'eip155:chainId:address'` |
| `from`                 | `string`                                 | UEA address that executed on Push Chain    |
| `to`                   | `string \| null`                         | Target address                             |
| `value`                | `bigint`                                 | Value transferred (in smallest unit)       |
| `data`                 | `string`                                 | Calldata                                   |
| `chainId`              | `string`                                 | Push Chain ID (`'42101'` on testnet)       |
| `blockNumber`          | `bigint`                                 | Block number                               |
| `nonce`                | `number`                                 | UEA nonce                                  |
| `atomic`               | `boolean`                                | `true` for a single tx or an atomic batch (EIP-7702 or UEA multicall); `false` only when a Push-native multicall fell back to sequential execution |
| `gasLimit`             | `bigint`                                 | Gas limit                                  |
| `gasPrice`             | `bigint`                                 | Gas price (wei)                            |
| `maxFeePerGas`         | `bigint`                                 | EIP-1559 max fee                           |
| `maxPriorityFeePerGas` | `bigint`                                 | EIP-1559 priority fee                      |
| `wait()`               | `(confirmations?) => Promise<TxReceipt>` | Wait for on-chain confirmation             |

> External chain fields (`externalTxHash`, `externalChain`, `externalExplorerUrl`) are only available on `TxReceipt` after `tx.wait()` - not on the initial `TxResponse`.

### TxReceipt (from `await tx.wait()`)

| Field                 | Type                               | Description                                 |
| --------------------- | ---------------------------------- | ------------------------------------------- |
| `status`              | `number`                           | `1` = success, `0` = failure                |
| `hash`                | `string`                           | Transaction hash                            |
| `blockNumber`         | `bigint`                           | Confirmed block number                      |
| `from`                | `string`                           | Executor address (UEA)                      |
| `to`                  | `string \| null`                   | Target address                              |
| `gasUsed`             | `bigint`                           | Actual gas consumed                         |
| `gasPrice`            | `bigint`                           | Gas price used                              |
| `logs`                | `array`                            | Emitted event logs                          |
| `contractAddress`     | `string \| null`                   | Deployed contract address (deploy txs only) |
| `raw`                 | `{ from, to, nonce, data, value }` | Raw on-chain data                           |
| `externalTxHash`      | `string` _(Route 2 only)_          | Hash on the external chain                  |
| `externalChain`       | `string` _(Route 2 only)_          | External chain identifier                   |
| `externalExplorerUrl` | `string` _(Route 2 only)_          | External explorer URL                       |

## Prepare + Execute - Multichain Cascade

Use `prepareTransaction` + `executeTransactions` when you need **multiple ordered hops across chains under a single user signature**. Each hop is prepared independently, then all are submitted together.

### `prepareTransaction`

`client.universal.prepareTransaction(tx)` → `Promise<PreparedUniversalTx>`

Accepts the **same arguments as `sendTransaction`** (same routing rules: Route 1 / Route 2 / Route 3 via `tx.to` and `tx.from`). Does **not** submit - returns a prepared object.

```ts
// Hop 0: call a contract on Push Chain (Route 1)
const hop0 = await client.universal.prepareTransaction({
  to: '0xContractOnPushChain',
  data: PushChain.utils.helpers.encodeTxData({
    abi: MyABI,
    functionName: 'increment',
  }),
});

// Hop 1: call a contract on BNB Testnet (Route 2)
const hop1 = await client.universal.prepareTransaction({
  to: {
    address: '0xContractOnBnb',
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  },
  data: PushChain.utils.helpers.encodeTxData({
    abi: MyABI,
    functionName: 'increment',
  }),
});
```

**`PreparedUniversalTx` shape:**

| Property       | Type                                                             | Description                                     |
| -------------- | ---------------------------------------------------------------- | ----------------------------------------------- |
| `route`        | `'UOA_TO_PUSH' \| 'UOA_TO_CEA' \| 'CEA_TO_PUSH' \| 'CEA_TO_CEA'` | Detected routing mode                           |
| `estimatedGas` | `bigint`                                                         | Estimated gas units                             |
| `nonce`        | `bigint`                                                         | Nonce for submission                            |
| `deadline`     | `bigint`                                                         | Signature expiry deadline                       |
| `payload`      | `string`                                                         | Encoded payload ready for `executeTransactions` |

---

### `executeTransactions`

`client.universal.executeTransactions(txs)` → `Promise<CascadedTxResponse>`

Submits all prepared hops as a single Push Chain transaction. The SDK coordinates downstream execution across chains.

```ts
const cascade = await client.universal.executeTransactions([hop0, hop1]);
console.log('Push Chain tx:', cascade.initialTxHash);
console.log('Hops:', cascade.hopCount);

// Wait for all hops to confirm
const result = await cascade.wait({
  progressHook: (e) =>
    console.log(`[Hop ${e.hopIndex}] ${e.status} on ${e.chain}`),
  pollingIntervalMs: 3000,
  timeout: 300_000, // 5 min
});
console.log('All complete:', result.success);
```

**`CascadedTxResponse` shape:**

| Property            | Type                               | Description                                                                                                 |
| ------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `initialTxHash`     | `string`                           | Hash of the user-signed Push Chain transaction; use this to reference it downstream                        |
| `initialTxResponse` | `UniversalTxResponse`              | Full response for the coordinating Push Chain tx; use this when you need nonce, gas, or block metadata     |
| `hops`              | `CascadeHopInfo[]`                 | All hops with routing and status                                                                            |
| `hopCount`          | `number`                           | Total hop count                                                                                             |
| `finalTxHash`       | `string` _(optional)_              | Final tx hash resolved by `waitForAll()` / `wait()` once cascade tracking completes                         |
| `wait(opts?)`       | `Promise<CascadeCompletionResult>` | Wait for all hops to confirm                                                                                |
| `waitForAll(opts?)` | `Promise<CascadeCompletionResult>` | Alias for `wait`                                                                                            |

**`CascadeHopInfo` per hop:**

| Property          | Type                                                  | Description                                                                                                                          |
| ----------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `hopIndex`        | `number`                                              | 0-indexed position                                                                                                                   |
| `route`           | `TransactionRouteType`                                | `'UOA_TO_PUSH'`, `'UOA_TO_CEA'`, `'CEA_TO_PUSH'`, or `'CEA_TO_CEA'`                                                                  |
| `executionChain`  | `CHAIN`                                               | Chain where this hop executes                                                                                                        |
| `expectedSubTxId` | `string` _(optional)_                                 | Expected `universalSubTxId`, computed deterministically from the parent; available before `txHash` resolves                          |
| `status`          | `'pending' \| 'submitted' \| 'confirmed' \| 'failed'` | Current status                                                                                                                       |
| `txHash`          | `string` _(optional)_                                 | Resolved transaction hash                                                                                                            |
| `outboundDetails` | `OutboundTxDetails` _(optional)_                      | Outbound hops only. Fields: `externalTxHash`, `destinationChain` (CHAIN), `explorerUrl`, `recipient`, `amount`, `assetAddr` (`address(0)` for native) |

**`CascadeCompletionResult` (from `wait()` / `waitForAll()`):**

| Property          | Type                                | Description                                                                  |
| ----------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
| `success`         | `boolean`                           | True if all hops confirmed                                                   |
| `hops`            | `CascadeHopInfo[]`                  | Final state of all hops                                                      |
| `finalTxHash`     | `string` _(optional)_               | Final tx hash for the last confirmed hop                                     |
| `finalTxResponse` | `CascadedTxResponse` _(optional)_   | Original cascade response, for consumers that need the full context          |
| `failedAt`        | `number` _(optional)_               | Index of first failed hop, if any                                            |

**`CascadeTrackOptions` (wait / waitForAll options):**

| Option              | Type                                       | Default   | Description                                                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pollingIntervalMs` | `number`                                   | `3000`    | Poll interval (ms)                                                                                                                                                                                                                                                                                                          |
| `timeout`           | `number`                                   | `300000`  | Total timeout (ms), default 5 min                                                                                                                                                                                                                                                                                          |
| `progressHook`      | `(event: CascadeProgressEvent) => void`    | -         | Per-hop callback: `{ hopIndex, route, chain, status, txHash, elapsed }`                                                                                                                                                                                                                                                     |
| `eventHook`         | `(event: ProgressEvent) => void`           | -         | Unified `ProgressEvent` stream for the cascade marker set (`001`, `002-xx`, `003-xx`, `203-xx`, `204-xx`, `209-xx`, `299-01`, `999-xx`, plus per-route awaiting/polling/success/failed/timeout). Cascade markers also fan out to the init-time `progressHook` on `PushChain.initialize`. Both channels are deduped if wired. |

> **No atomicity across hops** - if a downstream hop fails, earlier hops are already on-chain. Design contracts to handle partial execution.

> **Single signature** - `executeTransactions` submits one transaction to Push Chain; the SDK coordinates all downstream hops automatically.

---

## Track Transaction

### `trackTransaction` - resume tracking any tx by hash

`client.universal.trackTransaction(txHash, options)` → `Promise<UniversalTxResponse>`

Use this to re-check progress of a previously submitted transaction - after a page refresh, from a backend poller, or for any tx hash retrieved from storage. Works for transactions that originated on Push Chain **or** any external chain.

| Argument                             | Type                               | Default                                        | Description                                                               |
| ------------------------------------ | ---------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- |
| `txHash`                             | `string`                           | -                                              | Hash/signature of the transaction on its origin chain                     |
| `options.chain`                      | `CHAIN`                            | `PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT` | Chain where the tx was originally submitted                               |
| `options.progressHook`               | `(event) => void`                  | `undefined`                                    | Progress callback (same shape as `sendTransaction`)                       |
| `options.waitForCompletion`          | `boolean`                          | `true`                                         | `true` = wait for confirmation; `false` = return after first status check |
| `options.advanced.pollingIntervalMs` | `number`                           | `2000`                                         | Poll interval in ms (min `500`)                                           |
| `options.advanced.timeout`           | `number`                           | `60000`                                        | Max wait ms before timeout error                                          |
| `options.advanced.rpcUrls`           | `Partial<Record<CHAIN, string[]>>` | `{}`                                           | Custom RPC URLs for status queries                                        |

Returns the same `UniversalTxResponse` shape as `sendTransaction`.

> On Push Chain Donut, history-sensitive lookups (transactions/receipts pruned from the default RPC) transparently fall back to the archive RPC (`https://archive.evm.donut.rpc.push.org/`) - old tx hashes resolve without any extra option.

```ts
// Track a Push Chain tx
const response = await client.universal.trackTransaction(
  '0xbd765a6b60da077eaa89a382cd59c0469a4eaabcaca2707d3e6dcdeafc497a39',
  {
    progressHook: (p) => console.log(`${p.id}: ${p.message}`),
    advanced: { timeout: 30_000 },
  }
);

// Track an Ethereum Sepolia-origin tx
const response2 = await client.universal.trackTransaction(
  '0x9b4743376689eb6f90f3aeb9eea58381b3bcc033e1de4709281fd58a77b85098',
  { chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA }
);
```

### `tx.wait()` - wait for confirmation inline

After `sendTransaction`, call `.wait()` on the response:

```ts
try {
  const tx = await client.universal.sendTransaction({
    to: '0xRecipient',
    value: 1n,
  });
  const receipt = await tx.wait();
  if (receipt.status !== 1) throw new Error(`tx failed: ${receipt.hash}`);
  console.log('success');
} catch (e) {
  // Error codes and recovery actions: https://push.org/agents/errors.json
  console.error(e);
}
```

---

## Universal Read

Brings state from another blockchain (EVM or Solana) or from an HTTPS endpoint onto Push Chain, agreed on by validators and delivered on-chain: to the **Universal Read Registry** (`0x00000000000000000000000000000000000000b2`, the default) or to your own contract that inherits `UniversalReadClient` (see the push-contracts skill). A read is a **paid, asynchronous request**: gas plus a protocol fee and a callback budget, with the result arriving after validators reach quorum. If only your script needs the value and nothing on-chain acts on it, use an ordinary RPC call instead (see Read Blockchain State above).

| Method | Broadcasts | Use |
| ------ | ---------- | --- |
| `client.universal.read(subject, {options})` → `Promise<UniversalReadResponse>` | Yes, paid | Read one value |
| `client.universal.prepareRead(subject, {options})` → `Promise<PreparedRead>` | No, no funds needed | Quote a read; build a batch |
| `client.universal.executeReads(preparedReads, {options})` → `Promise<BatchReadResponse>` | Yes, paid | Several reads with one signature |
| `client.universal.trackRead({ requestId } \| { txHash }, {options})` → `Promise<UniversalReadResponse \| UniversalReadResponse[]>` | No, read-only client is enough | Resume after a restart or timeout |

### `read(subject, options)` - one value

`subject` is what to read: an account for a balance, a contract for a contract call or storage slot, or a URL for a Web2 read. `options.chain` is any supported EVM or Solana chain, or `CHAIN.WEB2`. Pass at most one query option; with none, `read` returns the native balance.

| Option | Reads |
| ------ | ----- |
| _(none)_ | Native balance of `subject` (ETH, BNB, SOL lamports) |
| `token` | ERC-20 balance (EVM, `token` = contract) or SPL balance (Solana, `token` = mint) of `subject` |
| `abi` or `idl` + `functionName` + `args` | Typed contract call on EVM (any function in the ABI) or a Solana program; `value` is decoded from the ABI or IDL |
| `idl` alone | A Solana program account at `subject`, decoded with the program's Anchor IDL |
| `storageSlot` | One EVM storage word of `subject` |
| `web2: { extract, method?, headers?, body?, timeoutMs? }` | JSON fields from an HTTPS endpoint (`chain: CHAIN.WEB2`), 1 to 16 `{ path, valueType, decimals? }` entries |

Other options: `callback` (your own receiver), `waitForCompletion` (default `true`), `progressHook`, and advanced `blockNumber` / `minConfirmations` (EVM only), `expiryBlocks` (default `300n`), `maxFee`, `refundTo`, `advanced.pollingIntervalMs`, `advanced.timeout` (at most 180 s by default), `advanced.enforceGasCheck`. Full tables: https://push.org/agents/workflows/universal-read.md

```typescript
const CHAIN = PushChain.CONSTANTS.CHAIN;

// Native balance (ETH)
await pushChainClient.universal.read(holder, { chain: CHAIN.ETHEREUM_SEPOLIA });

// Token balance: ERC-20 balanceOf(holder)
await pushChainClient.universal.read(holder, {
  chain: CHAIN.ETHEREUM_SEPOLIA,
  token: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
});

// Typed contract call: the ABI encodes the call and decodes the result
await pushChainClient.universal.read(tokenAddress, {
  chain: CHAIN.ETHEREUM_SEPOLIA,
  abi: [
    {
      type: 'function',
      name: 'totalSupply',
      stateMutability: 'view',
      inputs: [],
      outputs: [{ type: 'uint256' }],
    },
  ] as const,
  functionName: 'totalSupply',
});

// Storage slot
await pushChainClient.universal.read(contractAddress, {
  chain: CHAIN.ETHEREUM_SEPOLIA,
  storageSlot: 0n,
});
```

```typescript
const CHAIN = PushChain.CONSTANTS.CHAIN;

// Native balance (lamports)
await pushChainClient.universal.read(solanaHolder, { chain: CHAIN.SOLANA_DEVNET });

// Token balance: the holder's SPL token account for the mint
await pushChainClient.universal.read(solanaHolder, {
  chain: CHAIN.SOLANA_DEVNET,
  token: mintAddress,
});

// Program account, decoded with the program's Anchor IDL (the closest thing to a storage slot on Solana)
await pushChainClient.universal.read(accountAddress, {
  chain: CHAIN.SOLANA_DEVNET,
  idl: programIdl,
});
```

```typescript
const CHAIN = PushChain.CONSTANTS.CHAIN;

// Web2: JSON fields from an HTTPS endpoint
await pushChainClient.universal.read('https://jsonplaceholder.typicode.com/users/1', {
  chain: CHAIN.WEB2,
  web2: {
    extract: [
      { path: '$.id', valueType: 'uint256' },
      { path: '$.name', valueType: 'string' },
    ],
  },
});
```

### Submit, save references, check `value`

```ts
const pending = await client.universal.read('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
  waitForCompletion: false,
  progressHook: (p) => console.log(`${p.id}: ${p.title}`),
});
// Persist both before waiting; either one resumes the read from any process.
await db.save({ requestId: pending.requestId, txHash: pending.txHash });

const done = await pending.wait();
if (done.value !== undefined) {
  console.log('ETH:', ethers.formatEther(done.value));
} else {
  console.error('No value', {
    status: done.status,
    rawStatus: done.raw?.status,
    errorCode: done.raw?.errorCode,
    callbackDelivered: done.callbackDelivered,
    decodeError: done.decodeError,
  });
}
```

**`value` is the success signal.** The SDK sets it only when the read completed, the source returned data, your callback ran, and the bytes decoded. When `value` is `undefined`, the response says why:

| Field | What it tells you |
| ----- | ----------------- |
| `status` | Lifecycle of the request: `PENDING`, `VOTING`, `FULFILLED`, `EXPIRED`, `FAILED`, `ABORTED`. `FULFILLED` means the read completed; it does not by itself mean your callback succeeded. |
| `raw.status` | What the source returned: `READ.RESULT_STATUS.SUCCESS`, or `ERROR` with `raw.errorCode`. |
| `callbackDelivered` | Whether the receiver contract ran. `false` when it reverted or ran out of gas; `callbackFailReason` carries the revert data. |
| `decodeError` | Why the result bytes could not be decoded into `value`, for example an ABI that does not match the query. |

### Batch - `prepareRead` + `executeReads`

`prepareRead` takes the same subject and query options (not `waitForCompletion`, `progressHook` or `advanced`) and returns a `PreparedRead` with `fees.total`, `value`, `spec`, `specTuple` and `resultShape`. `executeReads` submits them as one transaction where the wallet supports atomic batching, otherwise as sequential transactions listed in `transactionHashes`. It returns `{ txHash, transactionHashes, reads, count, atomic, wait() }`; `atomic` describes submission only, so check `value` on every read.

```typescript
const CHAIN = PushChain.CONSTANTS.CHAIN;

const ethBalance = await pushChainClient.universal.prepareRead(holder, { chain: CHAIN.ETHEREUM_SEPOLIA });
const solBalance = await pushChainClient.universal.prepareRead(solanaHolder, { chain: CHAIN.SOLANA_DEVNET });

// Live progress for submission and every read in the batch.
const batch = await pushChainClient.universal.executeReads([ethBalance, solBalance], {
  progressHook: (event) => {
    console.log('[' + event.id + '] ' + event.level + ' - ' + event.title);
  },
});

const [eth, sol] = await batch.wait(); // results in prepared order
```

### Resume - `trackRead`

```typescript
const snapshot = await pushChainClient.universal.trackRead(
  { requestId },
  {
    progressHook: (progress) => {
      console.log(progress.id + ': ' + progress.title);
    },
  }
);

const done = await snapshot.wait();
console.log(done.status, done.value);
```

Pass `{ txHash }` instead to get an array of every read that transaction submitted. A typed contract call has no ABI on chain, so resume it with `resultShape`:

```typescript
const snapshot = await pushChainClient.universal.trackRead(
  { requestId },
  { resultShape: { kind: 'evmCall', abi: tokenAbi, functionName: 'totalSupply' } },
);
```

### Custom receiver - `callback`

```typescript
const result = await pushChainClient.universal.read(holder, {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
  callback: {
    target: receiverAddress,
    gasLimit: 200_000n,       // gas for _onReadResult, up to 1_000_000n
    abi: receiverAbi,
    functionName: 'request',  // your payable request entrypoint
  },
});

console.log(result.callbackDelivered); // true once _onReadResult ran
```

`callback.gasLimit` is the gas for your `_onReadResult` (default `500_000n`, at most `1_000_000n`), not for the request entrypoint. If the receiver gates its entrypoint, authorize `client.universal.account`, the `msg.sender` the receiver sees (for an external signer that is its UEA, not the wallet that deployed the contract). Receiver contract, `ReadSpec` and contract-built requests: see the push-contracts skill.

> **Rules**: `CHAIN.WEB2` is a read-only destination, never a `sendTransaction` target. URL, headers and body of a Web2 read are written to a public event log, so never include secrets. Validators only agree on identical Web2 bytes; extract stable fields or lower precision with `decimals`. A client timeout (`READ_TIMEOUT`) cancels nothing: resume with `trackRead`, do not resubmit. Failed requests are not retried automatically. The registry lives on Donut; elsewhere pass your own receiver through `callback`.

Examples: [EVM balance](https://push.org/agents/examples/universal-read-evm-balance.md), [ERC-20](https://push.org/agents/examples/universal-read-erc20-balance.md), [contract call](https://push.org/agents/examples/universal-read-contract-call.md), [storage slot](https://push.org/agents/examples/universal-read-storage-slot.md), [SOL](https://push.org/agents/examples/universal-read-sol-balance.md), [SPL](https://push.org/agents/examples/universal-read-spl-token.md), [Web2](https://push.org/agents/examples/universal-read-web2.md), [prepare](https://push.org/agents/examples/universal-read-prepare.md), [batch](https://push.org/agents/examples/universal-read-batch.md), [three sources](https://push.org/agents/examples/universal-read-batch-three-sources.md), [resume](https://push.org/agents/examples/universal-read-resume.md).

---

## Account & CEA Maintenance

### `migrateCEA(chain)` - upgrade a CEA to the latest version

`client.universal.migrateCEA(chain)` → `Promise<UniversalTxResponse>`

Migrates the **Chain Executor Account** (the contract that runs your Push-originated calls on an external chain) to the latest implementation. Sends a `MIGRATION_SELECTOR` payload via Route 2. Pass the external chain whose CEA should be upgraded.

```ts
const tx = await client.universal.migrateCEA(
  PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA
);
await tx.wait();
```

### `rescueFunds({ universalTxId, prc20 })` - recover funds stuck in the source Vault

`client.universal.rescueFunds(params)` → `Promise<UniversalTxResponse>`

When a CEA-to-Push (Route 3) inbound transaction fails, the bridged tokens stay locked in the source-chain Vault. `rescueFunds` triggers a manual TSS revert that releases them back to you.

| Arg             | Type             | Description                                                                  |
| --------------- | ---------------- | ---------------------------------------------------------------------------- |
| `universalTxId` | `0x${string}`    | `bytes32` hash of the failed inbound transaction                             |
| `prc20`         | `0x${string}`    | Push Chain PRC-20 token address whose source-chain counterpart is locked     |

```ts
const tx = await client.universal.rescueFunds({
  universalTxId: '0x...', // the failed inbound tx hash
  prc20: '0x...',         // PRC-20 whose locked source funds to release
});
await tx.wait();
```

---

## Sign a Message

```ts
const message = new TextEncoder().encode('Hello Push Chain'); // string also accepted; Uint8Array recommended
const signature = await client.universal.signMessage(message); // returns Uint8Array
// To get a hex string: Buffer.from(signature).toString('hex')
```

## Utility Functions

All utilities are available as `PushChain.utils.*` - no separate import needed.

### `parseUnits(value, exponent)` → `bigint`

Converts a human-readable token amount to its smallest unit.

| Argument   | Type                             | Description                                                 |
| ---------- | -------------------------------- | ----------------------------------------------------------- |
| `value`    | `string`                         | Human-readable amount, e.g. `'1.5'`                         |
| `exponent` | `number \| { decimals: number }` | Decimal places - `18` for PC/ETH, `6` for USDC, `8` for BTC |

**Returns**: `bigint` - e.g. `1500000000000000000n`

### `formatUnits(value, decimals)` → `string`

Converts a raw smallest-unit amount back to a human-readable string.

| Argument   | Type                                                 | Description                                                        |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------------ |
| `value`    | `bigint \| string`                                   | Raw amount in smallest units                                       |
| `decimals` | `number \| { decimals: number; precision?: number }` | Decimal places; pass an object to also round to `precision` places |

**Returns**: `string` - e.g. `'1.5'`, `'100.50'`

### `encodeTxData({ abi | idl, functionName, args })` → `string`

Encodes smart contract calldata without needing viem or ethers.js. Works for both EVM (ABI) and Solana (Anchor IDL) targets.

| Argument       | Type                 | Description                                                                                                                                                                |
| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `abi` \| `idl` | `any[]`              | EVM ABI array (`abi`) or Anchor IDL object (`idl`). Determines encoding: ABI → 4-byte selector + ABI-encoded args; IDL → 8-byte Anchor discriminator + Borsh-encoded args. |
| `functionName` | `string`             | Function (EVM) or instruction (Solana) name. Both `snake_case` and `camelCase` accepted for Solana.                                                                        |
| `args`         | `any[]` _(optional)_ | Function arguments, default `[]`. Use `bigint` for `uint256`/`u64`/`u128`.                                                                                                 |

**Returns**: `string` - hex-encoded calldata, e.g. `'0xd09de08a'`

```ts
// EVM target - use `abi` key
PushChain.utils.helpers.encodeTxData({
  abi,
  functionName: 'transfer',
  args: ['0xabc...', 1000n],
});
// args follow ABI types - use bigint for uint256 (passing 1000 instead of 1000n will fail strict TypeScript)
```

```ts
// Solana target - use `idl` key (not `abi`)
PushChain.utils.helpers.encodeTxData({
  idl: testCounterIdl, // Anchor IDL from target/idl/*.json
  functionName: 'receive_sol', // snake_case or camelCase both accepted
  args: [BigInt(0)], // use BigInt for u64/u128
});
```

---

### `getSupportedChains(pushNetwork)` → `{ chains: CHAIN[] }`

| Argument      | Type                               | Description      |
| ------------- | ---------------------------------- | ---------------- |
| `pushNetwork` | `PushChain.CONSTANTS.PUSH_NETWORK` | Network to query |

**Returns**: `{ chains }` - array of `PushChain.CONSTANTS.CHAIN.*` constant values

### `getSupportedChainsByName(pushNetwork)` → `{ chains: string[] }`

| Argument      | Type                               | Description      |
| ------------- | ---------------------------------- | ---------------- |
| `pushNetwork` | `PushChain.CONSTANTS.PUSH_NETWORK` | Network to query |

**Returns**: `{ chains }` - human-readable names, e.g. `['PUSH_TESTNET_DONUT', 'ETHEREUM_SEPOLIA', 'SOLANA_DEVNET', ...]`

### `getChainNamespace(chainName)` → `string`

| Argument    | Type     | Description               |
| ----------- | -------- | ------------------------- |
| `chainName` | `string` | e.g. `'ETHEREUM_SEPOLIA'` |

**Returns**: CAIP-2 namespace string, e.g. `'eip155:11155111'` - `undefined` if unsupported

### `getChainName(chainNamespace)` → `string`

| Argument         | Type     | Description                                |
| ---------------- | -------- | ------------------------------------------ |
| `chainNamespace` | `string` | CAIP-2 namespace, e.g. `'eip155:11155111'` |

**Returns**: Chain name string, e.g. `'ETHEREUM_SEPOLIA'` - `undefined` if unsupported

---

### `getMoveableTokens(chainOrClient?)` → `{ tokens: MoveableToken[] }`

Returns supported assets that can be moved across chains (used in `tx.funds.token`).

| Argument        | Type                                    | Description                        |
| --------------- | --------------------------------------- | ---------------------------------- |
| `chainOrClient` | `CHAIN \| PushChainClient` _(optional)_ | Filter tokens for a specific chain |

**Returns**: `{ tokens: Array<{ chain, symbol, decimals, address }> }`

### `getPayableTokens(chainOrClient?)` → `{ tokens: PayableToken[] }`

Returns supported assets that can be used to pay gas or fund token movement (used in `tx.payGasWith.token`).

| Argument        | Type                                    | Description                        |
| --------------- | --------------------------------------- | ---------------------------------- |
| `chainOrClient` | `CHAIN \| PushChainClient` _(optional)_ | Filter tokens for a specific chain |

**Returns**: `{ tokens: Array<{ chain, symbol, decimals, address }> }`

### `getPRC20Address(token, options?)` → `{ address, chain, symbol, decimals, network }`

Resolves the Push Chain synthetic PRC-20 address for a supported origin-chain token. Accepts either a `MoveableToken` (e.g., from `getMoveableTokens()`) or an object containing the origin `chain` and token `address`. Synchronous - PRC-20s live in the SDK's static token table. For tokens born **on** Push Chain (PC-20s), use [`getPC20Address`](#getpc20addressaddress-options--promisepc20addressresult) instead.

| Argument          | Type                                                  | Description                                                                                                                  |
| ----------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| _`token`_         | `MoveableToken \| { chain: string; address: string }` | Origin token - from `getMoveableTokens()` or explicit chain + address                                                        |
| `options.network` | `PushChain.CONSTANTS.PUSH_NETWORK`                    | Override the Push network. Defaults to client's initialized network. For example: `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET` |

**Returns**: `{ address: 0x${string}, chain: CHAIN, symbol: string, decimals: number, network: PUSH_NETWORK }`

```ts
// Using { chain, address }
const prc20Alt = PushChain.utils.tokens.getPRC20Address({
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
  address: '0x97F477B7f970D47a87B42869ceeace218106152a',
});
console.log('USDC.eth:', JSON.stringify(prc20Alt));

// Using a MoveableToken directly
const { tokens: moveable } = PushChain.utils.tokens.getMoveableTokens(
  PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA
);
const ethMoveable = moveable.find((t) => t.symbol === 'ETH');
const {
  address: prc20Addr,
  symbol,
  decimals,
  network,
} = PushChain.utils.tokens.getPRC20Address(ethMoveable);
```

### `getPC20Address(address, options)` → `Promise<PC20AddressResult>`

Resolves a **PC-20** (a token born on Push Chain) against UniversalCore's on-chain registry and lists every chain it is deployed on. Accepts either end of the mapping - the canonical Push-native token or one of its external wrappers - and always returns the canonical token at `.address`, with every **confirmed** deployment in `.registry` (a predicted first-export wrapper address never appears there).

Unlike `getPRC20Address`, this is **asynchronous**: PC-20 mappings are dynamic and live on chain. It is never symbol-based - PC-20 identity is address-and-chain only.

| Argument          | Type                                 | Description                                                                                                                                     |
| ----------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| _`address`_       | `string`                             | Canonical Push Chain token or an external wrapper address                                                                                        |
| _`options.network`_ | `PushChain.CONSTANTS.PUSH_NETWORK` | The Push network to resolve on, e.g. `PushChain.CONSTANTS.PUSH_NETWORK.TESTNET`                                                                  |
| `options.chain`   | `PushChain.CONSTANTS.CHAIN` _(optional)_ | Where the address lives. Omit to let the SDK discover it - throws `PC20AmbiguousAddressError` if more than one chain claims the address      |
| `options.rpcUrls` | `Partial<Record<CHAIN, string[]>>` _(optional)_ | Custom RPC URLs per chain for the registry and wrapper reads                                                                              |
| `options.strict`  | `boolean` _(optional)_               | Also run live factory-identity checks. Slower; off by default                                                                                    |

**Returns**: `{ address: 0x${string}, name: string, symbol: string, decimals: number, network: PUSH_NETWORK, registry: Array<{ address, chain, chainName }> }`

```ts
const token = await PushChain.utils.tokens.getPC20Address(
  '0x14693f665cE282A451ba9a86F2EC04B43F931145', // a PC-20 on Donut Testnet
  {
    chain: PushChain.CONSTANTS.CHAIN.PUSH_TESTNET_DONUT,
    network: PushChain.CONSTANTS.PUSH_NETWORK.TESTNET,
  }
);
console.log('canonical:', token.address, 'decimals:', token.decimals);
for (const entry of token.registry) {
  console.log(`${entry.chainName}: ${entry.address}`);
}
```

> Lookup is lenient; spending is not. `options.chain` may be omitted here, but `funds.token.chain` is **required** when sending, and one that disagrees with where the funds actually are is rejected before any approval. Throws `PC20WrapperNotRegisteredError`, `PC20RegistryMismatchError`, `PC20ExpectedButPRC20Error`, `PC20AmbiguousAddressError` - see [errors.json](https://push.org/agents/errors.json) for the full typed family.

---

### `toUniversal(address, { chain })` → `UniversalAccount`

Wraps an address and chain into a `UniversalAccount` object.

| Argument        | Type     | Description                   |
| --------------- | -------- | ----------------------------- |
| `address`       | `string` | Wallet address                |
| `options.chain` | `CHAIN`  | `PushChain.CONSTANTS.CHAIN.*` |

**Returns**: `{ chain: string, address: string }` - e.g. `{ chain: 'eip155:11155111', address: '0xAbc...' }`

### `toChainAgnostic(address, { chain })` → `string`

Produces a fully-qualified chain-agnostic address string.

| Argument        | Type     | Description                   |
| --------------- | -------- | ----------------------------- |
| `address`       | `string` | Wallet address                |
| `options.chain` | `CHAIN`  | `PushChain.CONSTANTS.CHAIN.*` |

**Returns**: `string` - e.g. `'eip155:11155111:0xAbc...'`

### `fromChainAgnostic(chainAgnosticAddress)` → `UniversalAccount`

Parses a chain-agnostic address string back into a `UniversalAccount`.

| Argument               | Type     | Description                                                   |
| ---------------------- | -------- | ------------------------------------------------------------- |
| `chainAgnosticAddress` | `string` | Full chain-agnostic string, e.g. `'eip155:11155111:0xAbc...'` |

**Returns**: `{ chain: string, address: string }`

### `deriveExecutorAccount(universalAccount, options?)` → `Promise<{ address, deployed? }>`

Derives a UEA on Push Chain from any origin account, or a CEA on an external chain from a Push/UOA account.

| Argument                   | Type                   | Description                                                                        |
| -------------------------- | ---------------------- | ---------------------------------------------------------------------------------- |
| `universalAccount`         | `UniversalAccount`     | Created via `toUniversal()`                                                        |
| `options.chain`            | `CHAIN` _(optional)_   | When provided, derives a CEA on that external chain instead of a UEA on Push Chain |
| `options.skipNetworkCheck` | `boolean` _(optional)_ | Deterministic derivation only, skip deployment check. Default `false`              |

**Returns**: `{ address: string, deployed?: boolean }` - `deployed` is included when `skipNetworkCheck` is `false`

### `resolveControllerAccount(account, options?)` → `Promise<{ accounts }>`

Reverse-maps any executor account (UEA or CEA) back to its origin controlling wallet. Complement of `deriveExecutorAccount` - forward is UOA→executor, this is executor→UOA. Prefer this over the older `convertExecutorToOriginAccount` (still exported but deprecated since 6.0.0): `resolveControllerAccount` handles UEA and CEA in one call, returns the full controller chain with `type` / `role` metadata, and supports `skipNetworkCheck` for deterministic-only resolution.

| Argument                   | Type                   | Description                                                                    |
| -------------------------- | ---------------------- | ------------------------------------------------------------------------------ |
| `account`                  | `string`               | Executor address - can be a UEA, CEA, or Push Chain account address            |
| `options.chain`            | `CHAIN` _(optional)_   | Required for CEA context - specifies the external chain the CEA is deployed on |
| `options.skipNetworkCheck` | `boolean` _(optional)_ | Deterministic resolution only, skip existence check. Default `false`           |

**Returns**: `Promise<{ accounts: Array<{ chain, chainName, address, type, exists, role? }> }>` - `type`: `'uea' | 'uoa' | 'cea'`; `role: 'controller'` marks the root controlling account.

```ts
// Resolve a UEA back to its controlling UOA
const { accounts } =
  await PushChain.utils.account.resolveControllerAccount('0xUEAAddress');
const controller = accounts.find((a) => a.role === 'controller');

// Resolve a CEA - chain is required
const { accounts: bnbAccounts } =
  await PushChain.utils.account.resolveControllerAccount('0xCEAAddress', {
    chain: PushChain.CONSTANTS.CHAIN.BNB_TESTNET,
  });
```

---

### `slippageToMinAmount(amount, { slippageBps })` → `string`

Calculates the minimum acceptable output amount given a slippage tolerance. Used when constructing `tx.payGasWith.minAmountOut` or validating swap quotes.

| Argument              | Type     | Description                                                     |
| --------------------- | -------- | --------------------------------------------------------------- |
| _`amount`_            | `string` | Input amount in smallest units, e.g. `'100000000'` for 100 USDC |
| `options.slippageBps` | `number` | Slippage in basis points - `100 = 1%`, `50 = 0.5%`              |

**Returns**: `string` - minimum out amount in smallest units, e.g. `'99000000'`

Full reference: https://push.org/agents/workflows/use-utility-functions.md

---

## Explorer Utilities

Available on any initialized client instance as `pushChainClient.explorer.*`. Works in both read-only and signing modes.

### `explorer.getTransactionUrl(txHash, options?)` → `string`

Returns a block explorer URL for any transaction hash.

| Argument        | Type                 | Description                                                                    |
| --------------- | -------------------- | ------------------------------------------------------------------------------ |
| `txHash`        | `string`             | Transaction hash                                                               |
| `options.chain` | `CHAIN` _(optional)_ | Specific chain explorer to use. Defaults to Push Chain (`PUSH_TESTNET_DONUT`). |

**Returns**: `string` - a full explorer URL, e.g. `'https://donut.push.network/tx/0x...'`

```ts
// Push Chain explorer URL (default)
const url = pushChainClient.explorer.getTransactionUrl(txHash);

// External chain explorer URL
const sepoliaUrl = pushChainClient.explorer.getTransactionUrl(txHash, {
  chain: PushChain.CONSTANTS.CHAIN.ETHEREUM_SEPOLIA,
});
```

### `explorer.listUrls(options?)` → `{ explorers }`

Lists block explorer URLs for one chain.

| Argument        | Type                 | Description                                                     |
| --------------- | -------------------- | --------------------------------------------------------------- |
| `options.chain` | `CHAIN` _(optional)_ | Chain to list explorers for. Defaults to client's origin chain. |

**Returns**: `{ explorers: Array<{ chain: string; chainName: string; urls: string[] }> }`

### `explorer.listAllUrls()` → `{ explorers }`

Lists block explorer URLs for **all** supported chains at once.

**Returns**: `{ explorers: Array<{ chain: string; chainName: string; urls: string[] }> }`

```ts
const { explorers } = pushChainClient.explorer.listAllUrls();
for (const { chainName, urls } of explorers) {
  console.log(chainName, urls[0]);
}
```

---

## Contract Helpers

> For **off-chain** UEA/CEA derivation from TypeScript, use `PushChain.utils.account.deriveExecutorAccount()` - see Utility Functions above. The UEAFactory below is for **on-chain Solidity** identity resolution. For the full Solidity contract pattern, see the `push-contracts` skill.

> The other contract helper is the **Universal Read Client** (`UniversalReadClient`), an abstract base contract a Push Chain contract inherits to receive [Universal Read](#universal-read) results in `_onReadResult`. It is inherited, not called; see the `push-contracts` skill and https://push.org/agents/workflows/use-contract-helpers.md.

### UEAFactory - Identity Resolution On-Chain

The Universal Executor Account Factory is deployed at a fixed address on Push Chain and lets your smart contract identify callers from external chains.

**Address**: `0x00000000000000000000000000000000000000eA`

```solidity
import "push-chain-core-contracts/src/Interfaces/IUEAFactory.sol";
// or define the interface manually - see docs
```

#### getOriginForUEA - Who called me?

```solidity
// Inside your contract - identify if msg.sender is a UEA (external chain user) or a native Push account
(UniversalAccountId memory origin, bool isUEA) =
    IUEAFactory(0x00000000000000000000000000000000000000eA).getOriginForUEA(msg.sender);

// origin.chainNamespace  → "eip155" | "solana" | ...
// origin.chainId         → e.g. "11155111" (Sepolia)
// origin.owner           → caller's original address in bytes (hex-encode for EVM, base58 for Solana)
// isUEA                  → false if msg.sender is a native Push Chain EOA
```

#### getUEAForOrigin - What is a user's UEA address?

```solidity
(address uea, bool isDeployed) = IUEAFactory(0x00000000000000000000000000000000000000eA).getUEAForOrigin(
    UniversalAccountId({
        chainNamespace: "eip155",
        chainId: "11155111",
        owner: abi.encodePacked(userAddress)
    })
);
// uea - deterministic UEA address (CREATE2), usable even before deployment
// isDeployed - true once the UEA has been deployed (first tx from that user)
```

Off-chain equivalent (SDK): `PushChain.utils.account.deriveExecutorAccount(universalAccount)`

Full reference: https://push.org/agents/workflows/use-contract-helpers.md

---

## Common Mistakes

| Symptom / Mistake                                            | Fix                                                                                                            |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `receipt.hash` is undefined / `tx` has no `.hash`            | `sendTransaction` returns a `TxResponse`, not a receipt - call `await tx.wait()` to get the receipt            |
| `wallet.sendTransaction()` used - tx reverts or is malformed | ethers/viem cannot produce a valid universal tx - replace with `client.universal.sendTransaction()`            |
| `PushChain.initialize()` rejects the signer                  | Raw ethers/viem wallet passed without wrapping - call `await PushChain.utils.signer.toUniversal(wallet)` first |
| `signMessage` return treated as a string                     | It returns `Uint8Array` - use `Buffer.from(sig).toString('hex')` if you need a hex string                      |
| Silent tx failure (no throw, no logs)                        | `tx.wait()` resolves even on reverts - always check `receipt.status === 1`                                     |
| Private key in source code                                   | Use `process.env.PRIVATE_KEY` - never hardcode keys in scripts or commit them to version control               |
| Treating `client.universal.account` as `{ address }` object — `account.address` returns `undefined` | `account` is a **plain address string**, not an object. Only `origin` has the `{ address, chain }` shape. Read it directly: ``const me = client.universal.account; // `0x${string}` ``. |
| Multicall rejected with `PushChainExecutionError` about a zero `to` address     | Only the **outer** `tx.to` is the zero address (multicall marker). Every entry inside the `data` array needs an explicit non-zero target. |
| PC-20 transfer misroutes or throws after adding `symbol` to `funds.token` | A PC-20 reference is exactly `{ chain, address }` - the SDK detects PC-20 vs `MoveableToken` by the **absence** of `symbol`. Resolve it first with `getPC20Address()`. |
| PC-20 transfer throws `PC20_TOKEN_CHAIN_MISMATCH` | `funds.token.chain` was set to the destination. It must be where the tokens sit **right now**; the destination goes in `to.chain`. |
| PC-20 wrapper address empty after an export | It was read from `receipt.externalAssetAddr`, a best-effort mirror that is `undefined` while the outbound is in flight (the raw chain field is only observed on a first deployment; the SDK backfills it from UniversalCore). Resolve wrappers from `getPC20Address(...).registry` - the authoritative record. |
| `PC20_EXPECTED_BUT_PRC20` thrown on a token transfer | A synthetic PRC-20 (`USDC.eth`, `pETH`) was passed as a PC-20 reference. External-born tokens move via their `MoveableToken` accessor, not `{ chain, address }`. |
| Universal Read result used without checking `value` | `value` is the success signal: it is set only when the read completed, the source returned data, the callback ran and the bytes decoded. When it is `undefined`, read `status`, `raw.status`, `callbackDelivered` and `decodeError`. |
| Universal Read resubmitted after `READ_TIMEOUT` - paid twice | A client timeout cancels nothing. Save `requestId` / `txHash` before `wait()` and resume with `client.universal.trackRead({ requestId })`. |
| `CHAIN.WEB2` passed to `sendTransaction` | `CHAIN.WEB2` is a read-only destination for `client.universal.read`. |
| Web2 read never reaches quorum | Validators need identical bytes. Extract stable fields, or lower numeric precision with `extract[].decimals`. |
| API key in a Web2 read URL or header | Request data is written to a public event log forever. Never include secrets. |

> For Solana targets, use `encodeTxData({ idl, functionName, args })` and pass the result as `tx.data` - same `{ to, value, data }` shape as EVM. The SDK resolves program accounts, PDAs, and the sender's CEA automatically from the IDL.
>
> For read-only queries, use ethers.js or viem directly with RPC `https://evm.donut.rpc.push.org/` - `@pushchain/core` is not required. When a Push Chain contract must act on state from another chain or a web API, use [Universal Read](#universal-read) instead.

## Downloadable Resources

Copy these files into your project - self-contained and ready to run:

| File                                                                                  | Purpose                                                                                |
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [`package.json`](https://push.org/agents/resources/push-backend/package.json)         | Dependencies: @pushchain/core, ethers, viem, @solana/web3.js, tsx                      |
| [`client-ethers.ts`](https://push.org/agents/resources/push-backend/client-ethers.ts) | ethers.js signer - Route 1 + Route 2                                                   |
| [`client-viem.ts`](https://push.org/agents/resources/push-backend/client-viem.ts)     | viem signer - Route 1, Route 2, cascade                                                |
| [`client-solana.ts`](https://push.org/agents/resources/push-backend/client-solana.ts) | Solana Keypair signer - Route 1 + Route 2 (Solana program via `encodeTxData({ idl })`) |

> [Resource index](https://push.org/agents/resources/push-backend/index.json) - machine-readable file list

## Extended Reference

- [Initialize Push Chain client](https://push.org/agents/workflows/initialize-client.md)
- [Create universal signer](https://push.org/agents/workflows/create-universal-signer.md)
- [Send universal transaction (all routes, advanced args)](https://push.org/agents/workflows/send-universal-transaction.md)
- [Send multichain transaction](https://push.org/agents/workflows/send-multichain-transaction.md)
- [Track transaction lifecycle](https://push.org/agents/workflows/track-transaction.md)
- [Read blockchain state](https://push.org/agents/workflows/read-blockchain-state.md)
- [Constants reference](https://push.org/agents/workflows/constants-reference.md)
- [Utility functions - full API](https://push.org/agents/workflows/use-utility-functions.md)
- [Contract helpers - UEAFactory and UniversalReadClient](https://push.org/agents/workflows/use-contract-helpers.md)
- [Universal Read (read, prepareRead, executeReads, trackRead, contract callbacks)](https://push.org/agents/workflows/universal-read.md)
- [Initialize with ethers.js example](https://push.org/agents/examples/initialize-client-ethers.md)
- [Initialize with viem example](https://push.org/agents/examples/initialize-client-viem.md)
- [Send to external chain example](https://push.org/agents/examples/send-transaction-external-chain.md)
