> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# On-Chain Verification

> Verify that token transfers actually happened on-chain by checking Transfer events. A transaction can succeed while the inner transfer fails silently.

## Why Verify Events?

On StarkNet, a transaction can have status **"SUCCEEDED"** while the token transfer inside it **fails silently**. This happens because:

1. The Chipi paymaster wraps your calls in `execute_from_outside_v2`
2. The forwarder contract executes the wrapped call
3. If the inner call fails (e.g., insufficient balance), the forwarder may still succeed
4. The transaction is marked "SUCCEEDED" on explorers

**Always verify Transfer events** to confirm tokens actually moved.

## Checking Transfer Events via RPC

After a transfer, query the transaction receipt for Transfer events from the token contract:

```typescript theme={null}
const provider = new RpcProvider({
  nodeUrl: "https://starknet-mainnet.infura.io/v3/YOUR_KEY",
});

const receipt = await provider.getTransactionReceipt(txHash);

if (receipt.execution_status !== "SUCCEEDED") {
  throw new Error(`Transaction failed: ${receipt.execution_status}`);
}

// Transfer event key = starknet_keccak("Transfer")
const TRANSFER_KEY =
  "0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9";

const USDC_ADDRESS =
  "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb";

const transferEvent = receipt.events.find((event) => {
  const isUsdc = BigInt(event.from_address) === BigInt(USDC_ADDRESS);
  const isTransfer = event.keys[0] === TRANSFER_KEY;
  return isUsdc && isTransfer;
});

if (!transferEvent) {
  throw new Error("No USDC Transfer event found — transfer failed silently");
}

// StarkNet Transfer events use indexed parameters (keys):
// keys[0] = Transfer selector
// keys[1] = from address
// keys[2] = to address
// data[0] = amount (low 128 bits)
// data[1] = amount (high 128 bits)
const from = transferEvent.keys[1];
const to = transferEvent.keys[2];
const amountLow = BigInt(transferEvent.data[0]);
const amountHigh = BigInt(transferEvent.data[1]);
const amount = amountLow + (amountHigh << 128n);

console.log(`Verified: ${amount} units from ${from} to ${to}`);
```

## Checking Balance Before and After

For additional certainty, query the token balance before and after the transfer:

```typescript theme={null}
const balanceBefore = await getBalance(provider, tokenAddress, recipientAddress);

const txHash = await serverClient.transfer({ params: { ... } });

// Wait for the tx to be included in a block
await provider.waitForTransaction(txHash);

const balanceAfter = await getBalance(provider, tokenAddress, recipientAddress);

if (balanceAfter <= balanceBefore) {
  throw new Error("Balance did not increase — transfer may have failed");
}

async function getBalance(provider, token, account) {
  const result = await provider.callContract({
    contractAddress: token,
    entrypoint: "balance_of",
    calldata: [account],
  });
  return BigInt(result[0]) + (BigInt(result[1]) << 128n);
}
```

## Using getTransactionStatus for Polling

The SDK's `getTransactionStatus` method polls the chain for finality, but it only checks **transaction inclusion** — not whether the inner transfer succeeded:

```typescript theme={null}
// This tells you the tx is on-chain, NOT that tokens moved
const status = await serverClient.getTransactionStatus({ hash: txHash });
// status.onChainStatus: "ACCEPTED_ON_L2" | "ACCEPTED_ON_L1" | "REJECTED" | ...
```

Use `getTransactionStatus` for finality, then verify Transfer events for correctness.

## Common Pitfalls

| Pitfall                                   | What Happens                   | How to Detect                               |
| ----------------------------------------- | ------------------------------ | ------------------------------------------- |
| Insufficient token balance                | Tx succeeds, no Transfer event | Check events in receipt                     |
| Wrong recipient address                   | Tokens sent to wrong address   | Verify `keys[2]` matches expected recipient |
| Amount overflow/underflow                 | Transfer of 0 tokens           | Check `data[0]` and `data[1]` are non-zero  |
| Tx status "SUCCEEDED" but transfer failed | Forwarder catches inner error  | Always check Transfer events                |

## Related

* [getTransactionStatus](/sdk/backend/methods/get-transaction-status) - Poll transaction finality
* [transfer](/sdk/backend/methods/transfer) - Send tokens
