# Providing credits

> Build and test an API credit provider that turns authorized credits into backed payments.

> **Experimental.** The example is for local integration testing, not production custody.

As an API credit provider, you let customers spend a balance you manage at participating APIs. You supply the payment's backing tokens and decide which spending requests to authorize. Merchants should not have to implement your internal credit ledger.

Your integration has two responsibilities: fund approved payments and restore credits only when their backing has actually returned.

## Start with the working example

The [Rust API credit provider example](https://github.com/solana-foundation/payment-channels/tree/23193ab8dc4198fd3d79a60d11371f7326f7aecb/examples/credit-provider) implements both responsibilities using the shared on-chain SDK. Customers deposit original SPL tokens into a vault before receiving credits. Customer and issuer signatures authorize a merchant, voucher signer, spending cap, nonce, and expiry.

From the payment-channels repository root:

```sh
cargo test --manifest-path examples/credit-provider/Cargo.toml --features client
just test-credit-provider-surfpool
```

Follow the [harness setup](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/scripts/credit-provider-e2e/README.md) first. The tested toolchain is Surfpool 1.5.0, Agave CLI 4.3.0, and platform-tools v1.58. Set `SOLANA_CLI` to the compatible executable if your default CLI is older.

The harness runs SBF v3 programs locally with ephemeral keys. It passes with the SDK-backed example and covers deposits, opening and top-ups, refund custody, failed-transaction rollback, authorization rejection, cancellation, and withdrawal followed by zero-refund completion. This is local execution evidence, not a production deployment.

## Use the on-chain SDK

The [payment-channel-interface crate](https://github.com/solana-foundation/payment-channels/tree/23193ab8dc4198fd3d79a60d11371f7326f7aecb/crates/interface) provides shared ABI definitions, read-only account readers, and callback validation. It is a `no_std` library without an entrypoint, allocator, or panic handler, so your program owns its runtime.

Use `validate_callback` with `CallbackExpectations` derived from your trusted configuration and stored authorization. The [example callback](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/examples/credit-provider/src/processor.rs#L262) shows the actual Rust integration. The SDK checks the channel, committed account configuration, callback authority, phase, and current sequence rather than making each provider copy binary layouts and validation code.

It does not manage your credit ledger, customer authorization, token custody, or durable replay protection. Those remain your program's responsibility. A valid channel callback proves its channel context, not the customer's permission to spend.

For direct on-chain reads, use `ChannelAccountView` with your trusted channel-program ID. Its `with_v1` and `with_v2` methods check ownership and layout on each read and keep the data borrow inside a closure. Copy out the values you need before making a CPI; then read again if you need current state. Callback validation uses this façade internally, so the example does not repeat those account checks.

This read-only helper returns an owned amount, not a borrowed account:

```rust
use payment_channel_interface::channel::ChannelAccountView;
use pinocchio::{error::ProgramError, AccountView, Address};

fn read_deposit(
    account: &AccountView,
    trusted_channel_program: &Address,
) -> Result<u64, ProgramError> {
    let channel = ChannelAccountView::new(account, trusted_channel_program)?;
    channel.with_v2(|view| Ok(view.deposit()))
    // The account-data borrow is released before this function returns.
}
```

Use the combined callback validator when processing a hook. This helper validates the mechanical callback contract only; it is **not** a complete credit-provider handler:

```rust
use payment_channel_interface::validation::{
    validate_callback, CallbackExpectations, ValidatedCallback,
};
use pinocchio::{error::ProgramError, AccountView};

fn inspect_callback(
    accounts: &[AccountView],
    payload: &[u8],
    trusted: &CallbackExpectations<'_>,
) -> Result<ValidatedCallback, ProgramError> {
    // Derive `trusted` from your stored policy and authorization,
    // not from the callback payload or caller-selected accounts.
    let callback = validate_callback(accounts, payload, trusted)?;

    // The caller must still check issuer/customer authorization,
    // cap, expiry, custody, refund amounts, and durable replay state.
    // Returning this context does not approve a transfer or issue credits.
    Ok(callback)
}
```

## Connect your accounting to the channel

The channel calls your program at configured lifecycle points. A **hook configuration** identifies the program, entrypoint, and exact accounts that the channel may call. It is committed when the channel opens.

The example uses two hooks:

| Hook             | Provider action                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `preFund`        | Validate the authorization, reduce credits, and move backing into the payer account before the channel takes the deposit. |
| `postDistribute` | Recover actual refunded tokens into custody, then restore credits. A zero refund restores zero credits.                   |

Use the example's [Rust instruction and hook builders](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/examples/credit-provider/src/client.rs) together with its [callback validation and transfers](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/examples/credit-provider/src/processor.rs).

Use the explicit hook instructions when creating and operating a channel with provider callbacks:

| Standard instruction | Instruction with hooks   | Discriminator |
| -------------------- | ------------------------ | ------------- |
| `open`               | `openWithHooks`          | 10            |
| `topUp`              | `topUpWithHooks`         | 11            |
| `distribute`         | `distributeWithHooks`    | 12            |
| `withdrawPayer`      | `withdrawPayerWithHooks` | 13            |

Standard instructions cannot bypass a channel's hooks. Once a channel is opened with the hook instruction family, funding and distribution must use that family even if all callback slots are disabled.

The [hook ABI reference](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/docs/lifecycle-hooks-v2.md) covers the wire format and account limits. [Upgrade compatibility](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/docs/versioning-and-upgrades.md) explains how existing channels stay usable.

## Decide the custody and recovery policy

The channel checks protected accounts immediately before and after each callback. A successful callback still fails the transaction if it changes channel state or escrow balance, removes merchant funds, or changes protected token-account controls. A refund callback may recover only the actual refund, preserving the payer's existing funds. The same rule applies when the payer is also a split recipient.

These checks are enforced by the channel, independently of whether your program uses the SDK. Read the [callback safety contract](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/docs/hook-safety-contract.md) for native-token refunds, delegated allowance, account aliases, and composition restrictions.

This limits callback effects on the operation's protected accounts. It does not protect a provider vault from its own upgrade authority or stop a required callback from blocking progress.

Before adapting the code, answer:

- Who can approve spending, and how are the merchant, amount, voucher key, and expiry bound to that approval?
- Where do backing tokens live, and what authority moves them?
- How do you prevent retries from spending or restoring credits twice?
- What happens if authorization expires, a delegate is revoked, your program pauses, or an upgrade changes its behavior?

The example requires bounded token delegation to recover refunds. Revoking that delegation blocks a positive refund transaction rather than issuing unbacked credits. It supports one active authorization per customer/mint, cancellation before funding, and original SPL Token only. It has no general credit-redemption or issuer-key-rotation flow. Review the [documented limits](https://github.com/solana-foundation/payment-channels/blob/23193ab8dc4198fd3d79a60d11371f7326f7aecb/examples/credit-provider/README.md) before reusing it.

## Submit your provider for review

Open a draft PR in [pay-kit](https://github.com/solana-foundation/pay-kit/pulls) with:

1. **The use case and supported scope:** networks, tokens, charge/session support, wallet capabilities, and sponsorship.
2. **Program and custody evidence:** source, deployment IDs, reproducible artifact hashes, upgrade authorities, authorization rules, and recovery policy.
3. **The integration:** a named provider policy, trusted hook/account resolver, transaction preparation, and independent payment verification. A name must select reviewed configuration, not customer-supplied hook JSON.
4. **Reproducible tests:** real funding and refunds, zero refunds, cap and replay rejection, revoked authority, rollback, and restart recovery. Include complete transaction sizes and compute usage.

Link a companion Pay PR for merchant configuration and client/operator behavior. Keep availability disabled until the integration is accepted. Changes to deployment, authority, hook policy, or supported capabilities need review too.

“Blessed” means maintainers accepted a specific implementation and configuration. It is not automatic registration, a security certification, or permission to claim support beyond that reviewed scope.
