> ## 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.

# SDP integration

> How Confiroll uses a vendored, headless Stellar Disbursement Platform (as a channel-account pool, fee-bump, submit, and retry), the queue relay, and the one upstream patch.

Confiroll vendors the **Stellar Disbursement Platform (SDP) v6.6.1** (Apache-2.0) and runs it
**headless**. Confiroll does *not* use it as a disbursement platform, and never calls SDP's
disbursement/HTTP payout API. Confiroll uses it purely as reliable submission infrastructure.

## What Confiroll uses SDP for

<CardGroup cols={2}>
  <Card title="Channel-account pool" icon="layer-group">
    A pool of pre-provisioned channel accounts that source outbound transactions (bootstrapped
    with `channel-accounts ensure`).
  </Card>

  <Card title="Fee-bump" icon="coins">
    SDP's transaction worker wraps a queued operation with a channel-account inner tx and
    fee-bumps it from the distribution account.
  </Card>

  <Card title="Submit" icon="paper-plane">
    Simulates against RPC, applies Soroban resources, and submits.
  </Card>

  <Card title="Retry" icon="rotate">
    The worker loop retries on transient failures.
  </Card>
</CardGroup>

The commands Confiroll relies on are the worker (`tss`), `channel-accounts`
(`create`/`ensure`/`verify`/`view`/`delete`), and `serve` (the headless HTTP surface for
health/toml). Confiroll leaves the disbursement, receiver, and payment flows of stock SDP unused.

<Note>
  Two commands run the moving parts. `channel-accounts ensure` provisions the pool of source
  accounts, and `tss` runs the worker that drains the queue and fee-bumps:

  ```bash theme={"system"}
  # provision (idempotently) a pool of channel accounts
  stellar-disbursement-platform channel-accounts ensure 3

  # run the transaction submission worker (drains the queue, fee-bumps, submits, retries)
  stellar-disbursement-platform tss
  ```
</Note>

## Why not the HTTP API

SDP's HTTP sponsored-transaction endpoint derives the sponsored account from the
caller's passkey, so it can only sponsor an SDP *embedded smart wallet*, with no field for an
employer's `G-address`. A confidential payout is sourced by a classic account, so the HTTP API
simply cannot express it. Because of this, Confiroll's relay **bypasses the HTTP API** and
writes directly into SDP's Postgres queue.

## The relay: writing straight to the queue

* It inserts a `PENDING`, `SPONSORED`-type row into `tss.submitter_transactions` (carrying the
  external id, tenant, sponsored account, sponsored operation XDR, and distribution account)
  via a `pg` client.
* SDP's unmodified TSS worker picks up the row and does the channel-source + distribution-fee
  fee-bump, simulate, submit, retry.
* The relay **holds no keys and signs nothing**.

```mermaid theme={"system"}
flowchart LR
    A[Relay pg client] -->|INSERT PENDING SPONSORED row| B[(tss.submitter_transactions)]
    B -->|worker claims row| C[Unmodified TSS worker]
    C -->|channel-source + distribution-fee fee-bump| D[Simulate + submit over RPC]
    D -->|SUCCESS| E[On-chain]
    D -->|transient failure| C
```

The same flow, step by step:

<Steps>
  <Step title="Insert a queued row">
    The relay's `pg` client inserts one `PENDING`, `SPONSORED`-type row into
    `tss.submitter_transactions`, carrying the external id, tenant, sponsored account, sponsored
    operation XDR, and distribution account. No key is used and nothing is signed here.
  </Step>

  <Step title="Worker claims it">
    SDP's unmodified TSS worker polls the table and claims the pending row.
  </Step>

  <Step title="Fee-bump from a channel account">
    The worker sources the operation on a channel account from the pool and builds a CAP-15
    fee-bump paid by the distribution account.
  </Step>

  <Step title="Simulate, submit">
    It simulates against RPC, applies Soroban resources, and submits.
  </Step>

  <Step title="Retry on transient failure">
    The worker loop retries transient failures until the transaction reaches a terminal state.
  </Step>
</Steps>

### Environment

The relay and worker are configured entirely through environment variables:

| Variable           | Purpose                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `SDP_DATABASE_URL` | Postgres connection the `pg` relay writes to (the `tss.submitter_transactions` queue the worker drains). |
| `SDP_TENANT_ID`    | The tenant the queued row belongs to.                                                                    |
| `SDP_DISTRIBUTION` | The distribution account that pays the fee-bump's outer fee.                                             |
| `SDP_CHANNEL`      | The channel account that sources the inner transaction.                                                  |

<Note>
  This queue-relay path powers the batch flow (`/batch`). The
  deployed, non-custodial write path for real payouts is **Fork B `/transfer`**, which
  fee-bumps a browser-signed transfer directly through the API sponsor, not through SDP. See
  [Fee sponsorship](/developers/fee-sponsorship).
</Note>

## The one upstream modification

Confiroll keeps SDP as close to upstream as possible. The single code change is in the TSS
handler `internal/transactionsubmission/sponsored_transaction_handler.go`:

Stock SDP validates the sponsored principal as a **contract (`C-`) address only**. Its
"embedded wallet" feature assumes a Soroban smart-wallet principal. Confiroll's confidential
payroll relays operations authorized by **classic accounts** too, so the patch widens that one
validation to also accept a valid **ed25519 (`G-`) address**:

```go theme={"system"}
// Confiroll self-host patch: also accept a valid ed25519 account (G-address),
// not just a contract (C-address), as the sponsored principal. The real auth
// principal is still enforced by the op's own auth entries below.
if !strkey.IsValidContractAddress(sponsoredAccount) &&
    !strkey.IsValidEd25519PublicKey(sponsoredAccount) {
    return nil, fmt.Errorf("sponsored account is not a valid contract or account address")
}
```

The rest of the handler is unchanged upstream logic. It still rejects ops requiring auth from
the channel or distribution account, re-sources the op to the distribution account, and builds
on a channel-account sequence. The vendored copy records the change in its
`MODIFICATIONS.md` and retains the upstream `LICENSE` (Apache-2.0). The vendored tree trims
tests, CloudFormation, and the Helm chart.

<Info>
  Everything provisioned on the SDP side (channel accounts, the tenant, the distribution
  account) is **operational** infrastructure. None of it can read a confidential amount or
  spend a user's balance; those depend on keys SDP never holds.
</Info>

## FAQ

<AccordionGroup>
  <Accordion title="Why write to the database instead of calling SDP's API?">
    SDP's HTTP sponsored-transaction endpoint derives the sponsored account from the caller's
    passkey, so it only ever sponsors an SDP embedded smart wallet and has no field for an
    employer's `G-address`. A confidential payout is sourced by a classic account, so the relay
    inserts a `PENDING`, `SPONSORED`-type row straight into `tss.submitter_transactions` and
    lets the unmodified worker fee-bump it.
  </Accordion>

  <Accordion title="How far does Confiroll diverge from upstream SDP?">
    One handler. The single change is in
    `internal/transactionsubmission/sponsored_transaction_handler.go`, widening the sponsored
    principal check to accept an ed25519 (`G-`) address as well as a contract (`C-`) address.
    Everything else is stock SDP v6.6.1 (Apache-2.0); the vendored copy records the diff in
    `MODIFICATIONS.md` and keeps the upstream `LICENSE`.
  </Accordion>

  <Accordion title="Is the SDP relay the path real payouts take?">
    No. The queue relay powers the batch flow (`/batch`). Real
    non-custodial payouts go through Fork B `POST /transfer`, where the API sponsor fee-bumps a
    browser-signed transfer directly. See [fee sponsorship](/developers/fee-sponsorship).
  </Accordion>
</AccordionGroup>
