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

# Fee sponsorship

> Fork B plus the CAP-15 fee-bump, and the sponsor guard-rails: contract allow-list, fee cap, per-account quota, and key separation.

Every action you take on Confiroll is gas-free. Confiroll achieves that with a **fee-bump**:
you sign the inner transaction, and a Confiroll-operated sponsor wraps it in a CAP-15 fee-bump
and pays the network fee. The sponsor never needs your secret. It only pays the *outer*
fee, and on-chain your XLM balance does not move.

<CardGroup cols={3}>
  <Card title="You sign" icon="signature">
    You keep custody. The inner transaction is sourced by your own account and signed on your
    device with `signTransaction`.
  </Card>

  <Card title="Sponsor pays" icon="hand-holding-dollar">
    A separate hot key builds and signs the CAP-15 outer envelope and pays the fee. It cannot
    read the amount or move your balance.
  </Card>

  <Card title="0 XLM for you" icon="wallet">
    On-chain, `fee_account` is the sponsor and your XLM delta is exactly `0`. A zero-XLM
    account can still transact.
  </Card>
</CardGroup>

## The fee-bump path

<Steps>
  <Step title="Client builds and signs the inner transaction">
    The browser builds a `confidential_transfer` with the **employer as tx source** and signs
    the envelope with `signTransaction` (Fork B). The amount is inside the proof; the secret
    never leaves the device.
  </Step>

  <Step title="POST /transfer { signedXDR }">
    The browser sends the signed inner transaction to `payroll-api` with a Bearer session.
  </Step>

  <Step title="API binds and validates">
    The API rejects the request unless the session is a Stellar (SEP-10) session and the inner
    transaction's `source` equals the session's account. You can only fee-bump **your own**
    transaction (otherwise `403`).
  </Step>

  <Step title="Sponsor validates, then fee-bumps">
    The sponsor runs its guard-rails **before the key is touched**, wraps the inner tx via
    <code>buildFeeBumpTransaction</code> (outer fee = inner `*` 2), signs the *bump* with the
    sponsor key, submits over Soroban RPC, and polls to `SUCCESS`.
  </Step>

  <Step title="Result">
    <code>{'{ hash, status: "SUCCESS" }'}</code>. On-chain, `fee_account` is the sponsor and the
    employer's XLM delta is **0**.
  </Step>
</Steps>

A minimal client call looks like this:

```http theme={"system"}
POST /transfer HTTP/1.1
Host: api.confiroll.com
Authorization: Bearer <stellar-session-jwt>
Content-Type: application/json

{ "signedXDR": "AAAAAgAAAAB..." }
```

```json theme={"system"}
{ "hash": "f621412b...", "status": "SUCCESS" }
```

## Guard-rails

The sponsor is a guard-railed relay, not an open one. The sponsor enforces each rail before it
signs:

<CardGroup cols={2}>
  <Card title="Contract allow-list" icon="list-check">
    Every `invokeHostFunction` op's target contract must be in the configured allow-list. An
    **empty** allow-list refuses to even construct the sponsor: "that would be an open
    relay."
  </Card>

  <Card title="Fee cap" icon="gauge-high">
    The inner fee must be under a maximum (default 0.5 XLM / 5,000,000 stroops). The sponsor
    refuses anything above, capping its exposure per transaction.
  </Card>

  <Card title="Per-account quota" icon="hashtag">
    A per-source counter (default 200 per process lifetime) limits how many transactions any
    one account can have sponsored, so a single account can't drain the sponsor.
  </Card>

  <Card title="Key separation" icon="scissors">
    The sponsor's hot signing key must hold no other authority. Construction fails if the
    sponsor pubkey is on the forbidden list (e.g. the deployer or a vault owner). The signing
    key sits behind a narrow interface that never exposes the secret to the caller.
  </Card>
</CardGroup>

### Guard-rail reference

| Rail                | Default value                                                                 | What it blocks                                                                                                                                |
| ------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Contract allow-list | The configured set of allowed contracts; an empty set refuses to construct    | Any `invokeHostFunction` whose target contract is not allow-listed. An empty list would make the sponsor an open relay, so it will not start. |
| Fee cap             | 0.5 XLM = 5,000,000 stroops                                                   | Any inner transaction whose fee exceeds the cap, bounding the sponsor's per-transaction exposure.                                             |
| Per-account quota   | 200 per process lifetime                                                      | Sponsored volume from a single source account beyond the quota. The counter increments only after an on-chain `SUCCESS`.                      |
| Key separation      | Sponsor pubkey must not be on the forbidden list (e.g. deployer, vault owner) | Reuse of a privileged key as the hot sponsor. The sponsor also refuses transactions sourced by itself.                                        |

Additional checks: the sponsor refuses transactions sourced by *itself*, requires at least one
operation, and only sponsors `invokeHostFunction` (and allow-listed-asset `changeTrust`) op
types: anything else is "not sponsorable." The sponsor increments the per-account quota counter
only **after** an on-chain `SUCCESS`, so refused or failed attempts never consume your quota.

### The CAP-15 outer fee

CAP-15 lets an outer envelope pay the fee for an inner transaction it wraps. Confiroll sets the
outer fee to **inner fee `*` 2**. The doubling gives the fee-bump headroom over the inner
transaction's own declared fee, which the network requires for the bump to be strictly higher.

<Note>
  Worked example: if the inner `confidential_transfer` declares a fee of 1,000,000 stroops
  (0.1 XLM), the sponsor builds an outer envelope with a fee of 2,000,000 stroops (0.2 XLM) and
  signs only that outer envelope. Your inner transaction is left byte-for-byte as you signed it,
  so its signature stays valid. The sponsor pays the 2,000,000 stroops; your account pays `0`.
</Note>

## Errors on /transfer

The API returns the shape `{ "error": string }`. The status codes you can see:

| Status | Meaning           | Cause                                                                                                                                         |
| ------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Malformed request | The body is missing `signedXDR` or the XDR does not parse into a valid inner transaction.                                                     |
| `401`  | No session        | The `Authorization: Bearer` header is missing, expired, or not a valid session JWT.                                                           |
| `403`  | Foreign source    | The inner transaction's `source` is not the session's account. You may only fee-bump your own transaction.                                    |
| `422`  | Sponsor refused   | A guard-rail rejected the transaction (non-allow-listed contract, fee over cap, quota exhausted, self-sourced, or a non-sponsorable op type). |
| `501`  | Privy session     | The session is a Privy (email) session. Only Stellar (SEP-10) sessions can transfer.                                                          |

<Note>
  The API binds `tx.source == session.sub`, so it rejects foreign-source fee-bumps with `403`
  before the sponsor ever sees them. A Privy (email) session cannot transfer: `/transfer`
  returns `501` for it, and a Stellar wallet (SEP-10) session is required.
</Note>

## The two fee-bump routes

There are two ways a signed transaction reaches the chain fee-sponsored:

<CardGroup cols={2}>
  <Card title="Fork B /transfer (deployed)" icon="circle-check">
    The API sponsor fee-bumps the browser-signed transfer directly, using the guard-rails
    above. This is the real, non-custodial write path, proven end-to-end on testnet.
  </Card>

  <Card title="SDP queue relay" icon="database">
    The relay inserts a row into SDP's submission queue and its worker
    fee-bumps from a channel account. The relay submits it through SDP with channel-account fee-bumps.
  </Card>
</CardGroup>

1. **Fork B `/transfer` (the deployed non-custodial path).** The API sponsor fee-bumps the
   browser-signed transfer directly, using the guard-rails above. This is proven end-to-end on
   testnet. Its request and error shapes are in the [API reference](/developers/api-reference).
2. **The headless SDP relay (the batch path).** The relay inserts
   signed operations into SDP's submission queue, and its worker fee-bumps them from a channel
   account. The relay submits it through SDP with channel-account fee-bumps. See
   [SDP integration](/developers/sdp-integration).

Both make your action cost **0 XLM**; Fork B is the real, non-custodial write path.

## FAQ

<AccordionGroup>
  <Accordion title="Does the sponsor ever hold my secret key?">
    No. You sign the inner transaction on your device, and the sponsor signs only the outer
    CAP-15 envelope with its own separate hot key. The two signatures are independent. Confiroll
    holds zero user keys, so it cannot move your balance or read your amount.
  </Accordion>

  <Accordion title="What happens to my quota if a fee-bump fails?">
    Nothing. The per-account quota counter increments only after an on-chain `SUCCESS`. A
    guard-rail refusal (`422`), a foreign-source rejection (`403`), or a transaction that fails
    on-chain does not consume any of your 200-per-process quota.
  </Accordion>

  <Accordion title="Why does my Privy login get a 501 on /transfer?">
    A Privy (email) session is not bound to a Stellar signing key, so it cannot produce the
    self-signed inner transaction that `/transfer` fee-bumps. Only a Stellar (SEP-10) session,
    where `sub` is your G-address, can transfer. Log in with a Stellar wallet to use `/transfer`.
  </Accordion>
</AccordionGroup>
