# Pay gas fees with Solana SPL tokens

On Solana, you can let users pay transaction fees directly in SPL tokens (for example USDC) instead of SOL. The Openfort paymaster signs the transaction as fee payer, and the user pays the fee on-chain in one of the SPL mints your policy accepts — your project balance is never debited.

This is the strategy shown in the dashboard as **Charge SPL tokens (Solana)** when creating a gas sponsorship.

## How it works

1. You create a gas sponsorship with `splTokens` set to the SPL mints your project accepts.
2. The end-user's transaction must include an SPL `TransferChecked` instruction paying Kora in one of those mints (your wallet/SDK constructs this).
3. The client calls [`signTransaction`](/docs/products/infrastructure/paymaster/solana/endpoints#signtransaction) or [`signAndSendTransaction`](/docs/products/infrastructure/paymaster/solana/endpoints#signandsendtransaction).
4. Openfort enforces the policy — the transaction must contain a `TransferChecked` to an accepted mint, otherwise the request is rejected.
5. Kora signs as fee payer and (optionally) broadcasts the transaction. The user pays the SPL fee on-chain; the project is not billed.

## Discover the accepted SPL tokens

Kora is the source of truth for which mints can be used for fee payment. Query it for the active list — only mints from this list can be added to a policy:

```bash
# devnet — use `mainnet-beta` for live mode
curl -X POST https://api.openfort.io/rpc/solana/devnet \
  -H "Authorization: Bearer $YOUR_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "getSupportedTokens",
    "params": [],
    "id": 1
  }'
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tokens": [
      "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    ]
  }
}
```

See the [`getSupportedTokens`](/docs/products/infrastructure/paymaster/solana/endpoints#getsupportedtokens) endpoint reference for details.

### Common SPL mints

| Network | Token | Mint address |
|---------|-------|--------------|
| **Solana mainnet-beta** | USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| **Solana devnet** | USDC | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` |

Always confirm the active list with `getSupportedTokens` for your environment before creating a policy.

## Create the gas sponsorship

::::steps

### Create a policy for the sponsorship rules

Create a [policy](/docs/configuration/policies) with the `sponsorSolTransaction` operation. Add Solana criteria (`solAddress`, `programId`, `mintAddress`, …) to restrict which transactions are eligible, or leave `criteria` empty to accept every Solana transaction:

```bash
curl -X POST https://api.openfort.io/v2/policies \
  -H "Authorization: Bearer $YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "project",
    "description": "Sponsor Solana transactions paid in SPL",
    "rules": [
      {
        "action": "accept",
        "operation": "sponsorSolTransaction",
        "criteria": []
      }
    ]
  }'
```

### Create the gas sponsorship with SPL tokens

Link the policy to a gas sponsorship and pass the accepted SPL mints in `strategy.splTokens`:

```bash
curl -X POST https://api.openfort.io/v2/fee-sponsorship \
  -H "Authorization: Bearer $YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pay gas in USDC on Solana",
    "strategy": {
      "sponsorSchema": "charge_custom_tokens",
      "splTokens": [
        "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
      ]
    },
    "policyId": "ply_..."
  }'
```

The response echoes the accepted mints in `strategy.splTokens`:

```json
{
  "id": "pol_...",
  "object": "feeSponsorship",
  "enabled": true,
  "strategy": {
    "sponsorSchema": "charge_custom_tokens",
    "splTokens": [
      "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    ]
  },
  "policyId": "ply_..."
}
```

:::tip
You can list multiple mints in `splTokens` to give end-users a choice of fee tokens. The user's transaction picks the actual fee mint by including an SPL `TransferChecked` to Kora in one of them.
:::

::::

## Construct the user transaction

Your client builds the user's transaction with two things:

* The business instructions (transfer, swap, …) with the user as signer.
* An SPL [`TransferChecked`](https://spl.solana.com/token#transferring-tokens) paying the Kora fee payer in one of the accepted mints. The amount should cover Kora's fee — use [`estimateTransactionFee`](/docs/products/infrastructure/paymaster/solana/endpoints#estimatetransactionfee) with `fee_token` set to the mint to get a quote.

Only `TransferChecked` (token program instruction `12`) is recognised — the legacy `Transfer` instruction is not, because `TransferChecked` carries the mint inline.

Then call [`signTransaction`](/docs/products/infrastructure/paymaster/solana/endpoints#signtransaction) or [`signAndSendTransaction`](/docs/products/infrastructure/paymaster/solana/endpoints#signandsendtransaction). If the policy enforces a specific contract scope, pass `policyId`:

```bash
curl -X POST https://api.openfort.io/rpc/solana/mainnet-beta \
  -H "Authorization: Bearer $YOUR_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "signAndSendTransaction",
    "params": {
      "transaction": "<base64-encoded transaction>",
      "policyId": "pol_..."
    },
    "id": 1
  }'
```

If the transaction doesn't include a `TransferChecked` to an accepted mint, the request is rejected with a policy-violation error.

## API reference

### Create gas sponsorship

`POST /v2/fee-sponsorship`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | No | Display name |
| `strategy.sponsorSchema` | string | Yes | Must be `charge_custom_tokens` |
| `strategy.splTokens` | string\[] | Yes | SPL mint addresses (base58, 32–44 chars) accepted as fee payment. Duplicates are rejected. Must come from `getSupportedTokens` |
| `policyId` | string | Yes | Policy ID (`ply_...`) with a `sponsorSolTransaction` rule |

:::warning
`splTokens` is mutually exclusive with `tokenContract` and `tokenContractAmount`. Mixing them returns `400 Bad Request`. `tokenContractAmount` is also rejected on Solana SPL policies because Kora computes the exchange rate at request time.
:::
