Sponsored token transfer¶
Move an ERC-20 token for a user who holds no native gas token. Your backend submits the transfer through Photon; the relayer pays the gas and the cost is charged to your prepaid balance in USD.
This pattern fits onboarding flows: a fresh wallet receives tokens, and can act on them before it ever holds ETH.
Prerequisites¶
- An API key with balance.
- The token contract address and the sender's tokens already in place. For a
transferFrom-style move, the sender must have approved your spender contract beforehand; for the simple case below, the tokens sit in an account your system controls.
1. Encode the transfer¶
The relayed call is ordinary calldata. For transfer(address,uint256):
import { encodeFunctionData, erc20Abi } from "viem";
const callData = encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: ["0xRecipient…", 25_000_000n], // 25 USDC (6 decimals)
});
2. Submit through Photon¶
curl -X POST https://photon.example.com/api/submit-tx \
-H "api-key: $EDITH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "0xTokenContract…",
"callData": "'"$CALL_DATA"'",
"value": "0",
"chainId": 84532,
"maxUSD": 0.50,
"label": "welcome-transfer:user-4821"
}'
maxUSD caps what this transaction may cost you — if gas spikes past it, the
transaction fails with TRANSACTION_001 instead of draining your balance.
label is free-form and comes back on the record; use it to tie transactions to
your own entities.
The response is immediate:
{
"success": true,
"data": { "txId": "68fa3450539a3c9d28bbca33", "estimatedCostUSD": 0.0071 },
"timestamp": "2026-07-29T12:00:00.000Z"
}
3. Track it to completion¶
Either poll Get transaction status until
status is EXECUTED, or receive the same terminal event on a
webhook / WebSocket.
The executed record carries executionTxHash (link it to a block explorer for
your user) and costUSD — the actual amount charged.
If the transfer would revert (say, insufficient token balance), Photon's
preflight catches it and the record lands on FAILED with revert details when
you pass includeRevertInfo=true — your balance is not charged for
deterministic reverts caught before broadcast.
Variations¶
- Wait inline instead of polling — use
Submit and wait with a
timeoutMsup to 120000 when a synchronous UX is acceptable. - Many transfers at once — set
shouldBatchInMulticall: trueon each and Photon executes them as one atomic batch; see Batch transactions.