Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

API gateway, usage metering, and billing services for the Callora API marketplace. Talks to Soroban contracts and Horizon for on-chain settlement.

## Fee Abstraction

Developers can pay Stellar transaction fees using app tokens. The backend wraps their inner transaction in a Stellar fee-bump envelope signed by the platform fee account.

- `POST /api/billing/fee-abstraction/quote` – returns estimated XLM fee and app-token equivalent.
- `POST /api/billing/fee-abstraction` – accepts app-token payment reference and returns a signed fee-bump XDR.

Requires `FEE_BUMPER_SECRET_KEY` environment variable (Stellar secret key `S...`).

See [docs/fee-abstraction.md](./docs/fee-abstraction.md) for full API reference, security considerations, rate limits, and emitted events.

## Developer Profile Endpoints

- `GET /api/developers/me` returns the authenticated developer profile and auto-creates a blank profile row on first access.
Expand Down
160 changes: 160 additions & 0 deletions docs/fee-abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Fee Abstraction API

The fee-abstraction service lets developers pay Stellar transaction fees using app tokens rather than holding XLM. The backend wraps the developer's inner transaction in a Stellar fee-bump transaction signed by the platform fee account.

## Overview

1. Developer builds and signs an inner Stellar transaction.
2. Developer calls `POST /api/billing/fee-abstraction/quote` to get the XLM fee and its app-token equivalent.
3. Developer submits an app-token payment for that amount (off-chain).
4. Developer calls `POST /api/billing/fee-abstraction` with the inner XDR and the payment reference.
5. The backend creates and signs a fee-bump transaction; the caller receives the signed XDR for submission to Horizon.

---

## Endpoints

### `POST /api/billing/fee-abstraction/quote`

Returns an estimated fee for wrapping the supplied inner transaction.

**Authentication:** Bearer token required.

**Request body:**

```json
{
"innerXdr": "<base64-encoded Stellar transaction XDR>"
}
```

**Response `200`:**

```json
{
"baseFeeStroops": 100,
"feeBumpFeeStroops": 600,
"feeBumpFeeXlm": "0.0000600",
"appTokenAmount": "0.0000060",
"network": "testnet"
}
```

| Field | Description |
|---|---|
| `baseFeeStroops` | Per-operation base fee in stroops |
| `feeBumpFeeStroops` | Total outer fee for the fee-bump envelope |
| `feeBumpFeeXlm` | `feeBumpFeeStroops` expressed in XLM |
| `appTokenAmount` | Equivalent app-token amount to charge (based on current XLM/token rate) |
| `network` | Active Stellar network (`testnet` or `mainnet`) |

**Errors:**

| Status | Code | When |
|---|---|---|
| `400` | `VALIDATION_ERROR` | `innerXdr` missing, empty, or not a valid Stellar transaction XDR |
| `401` | `UNAUTHORIZED` | Missing or invalid Bearer token |

---

### `POST /api/billing/fee-abstraction`

Creates and signs a fee-bump transaction wrapping the supplied inner transaction.

**Authentication:** Bearer token required.

**Request body:**

```json
{
"innerXdr": "<base64-encoded Stellar transaction XDR>",
"appTokenPaymentTxId": "<payment reference confirming app-token deduction>"
}
```

**Response `200`:**

```json
{
"feeBumpXdr": "<signed fee-bump transaction XDR>",
"feeAccountPublicKey": "G...",
"feeStroops": 600
}
```

| Field | Description |
|---|---|
| `feeBumpXdr` | Signed fee-bump transaction XDR; submit directly to Horizon |
| `feeAccountPublicKey` | Public key of the platform fee account |
| `feeStroops` | Total fee charged by the fee-bump envelope |

**Errors:**

| Status | Code | When |
|---|---|---|
| `400` | `VALIDATION_ERROR` | Missing/empty fields or invalid `innerXdr` |
| `401` | `UNAUTHORIZED` | Missing or invalid Bearer token |
| `500` | `INTERNAL_SERVER_ERROR` | Fee-bumper not configured or signing failed |

---

## Fee Calculation

The outer fee-bump fee is calculated as:

```
feeBumpFeeStroops = BASE_FEE × FEE_BUMP_MULTIPLIER × (inner_op_count + 1)
```

- `BASE_FEE` defaults to `100` stroops (override via `STELLAR_BASE_FEE`).
- `FEE_BUMP_MULTIPLIER` is `3` (hardcoded to ensure the fee-bump envelope is competitive).
- The app-token equivalent uses an approximate XLM → app-token exchange rate of `0.10 USDC/XLM` (for indicative quoting only).

---

## Security Considerations

- **Signing key**: The fee account's Stellar secret key is read from `FEE_BUMPER_SECRET_KEY` at runtime. Store this as a secrets-manager or environment secret—never commit it to source control.
- **Authentication**: Both endpoints require a valid developer Bearer token. Unauthenticated requests are rejected with `401`.
- **No double-spend protection**: The `appTokenPaymentTxId` field is recorded in the `fee_abstraction.executed` event for audit purposes but is not validated against an on-chain payment in this initial version. Callers must ensure the payment has been deducted before invoking the execution endpoint.
- **Network isolation**: The backend only builds transactions for the configured `STELLAR_NETWORK`. Cross-network mixing is rejected.

---

## Rate Limiting

The fee-abstraction endpoints are mounted under `/api/billing` and inherit the same REST rate limit applied to all billing routes:

- Window: `REST_RATE_LIMIT_WINDOW_MS` (default `60000` ms)
- Max requests: `REST_RATE_LIMIT_MAX_REQUESTS` (default `100`)
- Key: `user:<userId>` for authenticated requests, `ip:<ip>` fallback

When the limit is exceeded, a `429 Too Many Requests` response is returned with a `Retry-After` header.

---

## Emitted Events

After a successful execution, the `fee_abstraction.executed` event is emitted:

```ts
{
userId: string; // authenticated developer ID
appTokenPaymentTxId: string; // payment reference from the request
feeAccountPublicKey: string; // public key of the fee account
feeStroops: number; // total fee paid in stroops
feeBumpXdr: string; // signed fee-bump XDR
}
```

This event can trigger downstream webhook deliveries if the developer has subscribed to `fee_abstraction.executed` events.

---

## Environment Variables

| Variable | Required | Description |
|---|---|---|
| `FEE_BUMPER_SECRET_KEY` | **Yes** | Stellar secret key (`S...`) for the platform fee account |
| `STELLAR_BASE_FEE` | No (default `100`) | Base fee per operation in stroops |
| `STELLAR_NETWORK` | No (default `testnet`) | Active network: `testnet` or `mainnet` |
202 changes: 202 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,122 @@
}
}
}
},
"/api/billing/fee-abstraction/quote": {
"post": {
"summary": "Get fee-abstraction quote",
"description": "Returns the estimated XLM fee and equivalent app-token amount for wrapping a given inner Stellar transaction with a fee-bump.",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeAbstractionQuoteRequest"
}
}
}
},
"responses": {
"200": {
"description": "Quote calculated successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeAbstractionQuoteResponse"
}
}
}
},
"400": {
"description": "Invalid inner transaction XDR",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/api/billing/fee-abstraction": {
"post": {
"summary": "Execute fee abstraction",
"description": "Accepts the app-token payment and performs server-side fee bumping on the supplied inner Stellar transaction. Returns the signed fee-bump XDR ready for submission to Horizon.",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeAbstractionExecuteRequest"
}
}
}
},
"responses": {
"200": {
"description": "Fee-bump transaction created and signed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FeeAbstractionExecuteResponse"
}
}
}
},
"400": {
"description": "Invalid request body or inner transaction XDR",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Fee-bumper configuration or signing error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
},
"components": {
Expand All @@ -985,6 +1101,92 @@
}
},
"schemas": {
"FeeAbstractionQuoteRequest": {
"type": "object",
"required": [
"innerXdr"
],
"properties": {
"innerXdr": {
"type": "string",
"minLength": 1,
"description": "Base64-encoded XDR of the inner Stellar transaction"
}
}
},
"FeeAbstractionQuoteResponse": {
"type": "object",
"required": [
"baseFeeStroops",
"feeBumpFeeStroops",
"feeBumpFeeXlm",
"appTokenAmount",
"network"
],
"properties": {
"baseFeeStroops": {
"type": "number",
"description": "Base fee per operation in stroops"
},
"feeBumpFeeStroops": {
"type": "number",
"description": "Total fee-bump outer fee in stroops"
},
"feeBumpFeeXlm": {
"type": "string",
"description": "Fee-bump fee expressed in XLM"
},
"appTokenAmount": {
"type": "string",
"description": "Equivalent app-token amount to charge the developer"
},
"network": {
"type": "string",
"description": "Active Stellar network (testnet or mainnet)"
}
}
},
"FeeAbstractionExecuteRequest": {
"type": "object",
"required": [
"innerXdr",
"appTokenPaymentTxId"
],
"properties": {
"innerXdr": {
"type": "string",
"minLength": 1,
"description": "Base64-encoded XDR of the inner Stellar transaction to wrap"
},
"appTokenPaymentTxId": {
"type": "string",
"minLength": 1,
"description": "Transaction ID confirming the developer's app-token payment"
}
}
},
"FeeAbstractionExecuteResponse": {
"type": "object",
"required": [
"feeBumpXdr",
"feeAccountPublicKey",
"feeStroops"
],
"properties": {
"feeBumpXdr": {
"type": "string",
"description": "Signed fee-bump transaction XDR ready for submission to Horizon"
},
"feeAccountPublicKey": {
"type": "string",
"description": "Stellar public key of the fee-paying account"
},
"feeStroops": {
"type": "number",
"description": "Total fee charged in stroops"
}
}
},
"BillingDeductRequest": {
"type": "object",
"required": [
Expand Down
Loading