Skip to content
Closed
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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Thanks for contributing to the backend for Access Layer, a Stellar-native creato

- Read the [README](./README.md) for context.
- Review the [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md).
- Review the [Protocol Config endpoint](./docs/protocol-config.md) for bps fee field units and valid ranges.
- Review the scoped backlog in [docs/open-source/issue-backlog.md](./docs/open-source/issue-backlog.md).
- Keep pull requests limited to one backend issue or one documentation improvement.
- Open a discussion before changing core API shape or background processing architecture.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The server is responsible for:
- notifications, analytics, and moderation workflows
- access checks for gated off-chain content

See [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md) for a technical overview and [API Versioning](./docs/api-versioning.md) for details on schema versioning.
See [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md) for a technical overview, [API Versioning](./docs/api-versioning.md) for details on schema versioning, and [Protocol Config](./docs/protocol-config.md) for the bootstrap config endpoint including basis-points fee fields.

## Tech

Expand Down
91 changes: 91 additions & 0 deletions docs/protocol-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Protocol Config Endpoint

## Overview

`GET /api/v1/config` returns a read-only bootstrap payload clients fetch once on startup. No authentication required.

## Response shape

```json
{
"success": true,
"data": {
"environment": "development",
"apiVersion": "v1",
"network": "testnet",
"features": {
"walletConnect": true,
"emailVerification": true,
"googleOAuth": true
},
"display": {
"appName": "AccessLayer",
"supportEmail": "[email protected]"
},
"fees": {
"platformFeeBps": 250,
"maxCreatorRoyaltyBps": 1000,
"bpsDenominator": 10000
}
}
}
```

## Basis points (bps) fields

All values inside `fees` are expressed in **basis points**.

| 1 bps | 0.01% |
|-------|-------|
| 100 bps | 1% |
| 10000 bps | 100% |

Valid range for all bps fields: **0 – 10000** (inclusive).

To convert to a percentage:

```
percent = (feeBps / bpsDenominator) * 100
```

### `platformFeeBps`

Platform fee applied to every key purchase. Set by the protocol; clients must treat it as read-only.

- Default: `250` (2.50%)
- Range: 0–10000

### `maxCreatorRoyaltyBps`

Ceiling for creator-configured royalties on secondary sales. Creators may set any royalty up to this value; the contract rejects values above it.

- Default: `1000` (10.00%)
- Range: 0–10000

### `bpsDenominator`

Always `10000`. Exposed explicitly so clients can derive percentages without hardcoding the divisor.

## Usage example

```ts
const { fees } = await fetchProtocolConfig();
const platformFeePercent = (fees.platformFeeBps / fees.bpsDenominator) * 100;
// 2.5
```

## Validation locally

```bash
curl http://localhost:3000/api/v1/config | jq '.data.fees'
```

Expected:

```json
{
"platformFeeBps": 250,
"maxCreatorRoyaltyBps": 1000,
"bpsDenominator": 10000
}
```
78 changes: 78 additions & 0 deletions src/modules/config/config.controllers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// src/modules/config/config.controllers.test.ts
import { Request, Response } from 'express';
import { httpGetProtocolConfig } from './config.controllers';

jest.mock('../../config', () => ({
envConfig: { MODE: 'development' },
}));

describe('httpGetProtocolConfig', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let jsonMock: jest.Mock;
let statusMock: jest.Mock;

beforeEach(() => {
jsonMock = jest.fn();
statusMock = jest.fn().mockReturnValue({ json: jsonMock });
mockRequest = {};
mockResponse = { status: statusMock };
});

it('returns 200 with success envelope', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

expect(statusMock).toHaveBeenCalledWith(200);
expect(jsonMock).toHaveBeenCalledWith(
expect.objectContaining({ success: true })
);
});

it('includes fees object with bps fields', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

const payload = jsonMock.mock.calls[0][0];
const { fees } = payload.data;

expect(fees).toBeDefined();
expect(typeof fees.platformFeeBps).toBe('number');
expect(typeof fees.maxCreatorRoyaltyBps).toBe('number');
expect(typeof fees.bpsDenominator).toBe('number');
});

it('bps fields are within valid range [0, 10000]', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

const { fees } = jsonMock.mock.calls[0][0].data;

expect(fees.platformFeeBps).toBeGreaterThanOrEqual(0);
expect(fees.platformFeeBps).toBeLessThanOrEqual(10000);

expect(fees.maxCreatorRoyaltyBps).toBeGreaterThanOrEqual(0);
expect(fees.maxCreatorRoyaltyBps).toBeLessThanOrEqual(10000);
});

it('bpsDenominator is always 10000', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

const { fees } = jsonMock.mock.calls[0][0].data;

expect(fees.bpsDenominator).toBe(10000);
});

it('platformFeeBps converts correctly to percentage via bpsDenominator', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

const { fees } = jsonMock.mock.calls[0][0].data;
const percent = (fees.platformFeeBps / fees.bpsDenominator) * 100;

expect(percent).toBe(2.5);
});

it('sets network to testnet in development', () => {
httpGetProtocolConfig(mockRequest as Request, mockResponse as Response);

const { network } = jsonMock.mock.calls[0][0].data;
expect(network).toBe('testnet');
});
});
31 changes: 31 additions & 0 deletions src/modules/config/config.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ interface ProtocolConfig {
appName: string;
supportEmail: string;
};
/**
* Protocol fee parameters expressed in basis points (bps).
*
* 1 bps = 0.01%. All values are integers in the range [0, 10000].
* Clients must treat these as read-only; they are set by the protocol
* and change only via a contract governance action.
*/
fees: {
/**
* Platform fee charged on each key purchase.
* 250 = 2.50%. Valid range: 0–10000.
*/
platformFeeBps: number;
/**
* Maximum creator royalty a creator may configure on secondary sales.
* Creators may set any value up to this ceiling.
* 1000 = 10.00%. Valid range: 0–10000.
*/
maxCreatorRoyaltyBps: number;
/**
* Denominator used to convert a bps integer to a decimal fraction.
* Always 10000. Included so clients can derive percentages without
* hardcoding the divisor: `fee% = feeBps / bpsDenominator * 100`.
*/
bpsDenominator: number;
};
}

export const httpGetProtocolConfig = (
Expand All @@ -45,6 +71,11 @@ export const httpGetProtocolConfig = (
appName: 'AccessLayer',
supportEmail: '[email protected]',
},
fees: {
platformFeeBps: 250,
maxCreatorRoyaltyBps: 1000,
bpsDenominator: 10000,
},
};

res.status(200).json({
Expand Down
Loading