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
195 changes: 193 additions & 2 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ tags:
description: Goal-based investing — a target amount + date driving the agent's strategy selection
- name: admin
description: Admin-only management endpoints
- name: sub-accounts
description: Family & team sub-accounts with scoped, revocable permissions
- name: metrics
description: Prometheus-compatible metrics endpoint

Expand Down Expand Up @@ -1086,7 +1088,9 @@ paths:
summary: Initiate an on-chain deposit
description: |
Submits a deposit transaction to the active protocol on behalf of the
authenticated user. The user must match the userId in the request body.
authenticated user. The user must match the userId in the request body,
OR the caller must hold an active sub-account with DEPOSIT permission
for the target user.
security:
- BearerAuth: []
requestBody:
Expand Down Expand Up @@ -1314,7 +1318,9 @@ paths:
summary: Initiate an on-chain withdrawal
description: |
Submits a withdrawal transaction from the active protocol on behalf of
the authenticated user. The user must match the userId in the request body.
the authenticated user. The user must match the userId in the request body,
OR the caller must hold an active sub-account with WITHDRAW permission
for the target user.
security:
- BearerAuth: []
requestBody:
Expand Down Expand Up @@ -2859,6 +2865,147 @@ paths:
'404':
$ref: '#/components/responses/NotFound'

# ── Sub-Accounts ───────────────────────────────────────────────────────────

/api/v1/sub-accounts:
post:
tags: [sub-accounts]
operationId: createSubAccount
summary: Create a sub-account relationship
description: |
Creates a parent→child sub-account link with an initial permission set.
The authenticated user becomes the parent. The child must exist and must
not already be a parent (no chained sub-accounts).
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [childUserId, permissions]
properties:
childUserId:
type: string
format: uuid
description: The child user's ID
permissions:
type: array
items:
$ref: '#/components/schemas/SubAccountPermission'
minItems: 1
maxItems: 4
description: Initial permission set
example:
childUserId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
permissions: [VIEW, DEPOSIT]
responses:
'201':
description: Sub-account created
content:
application/json:
schema:
type: object
properties:
subAccount:
$ref: '#/components/schemas/SubAccount'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Child user not found
'409':
description: Sub-account relationship already exists

/api/v1/sub-accounts/{id}/permissions:
patch:
tags: [sub-accounts]
operationId: updateSubAccountPermissions
summary: Update sub-account permissions
description: |
Replaces the permission set for an existing sub-account. Only the
parent who owns the sub-account can update permissions.
security:
- BearerAuth: []
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: Sub-account ID
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [permissions]
properties:
permissions:
type: array
items:
$ref: '#/components/schemas/SubAccountPermission'
minItems: 1
maxItems: 4
example:
permissions: [VIEW, DEPOSIT, WITHDRAW]
responses:
'200':
description: Permissions updated
content:
application/json:
schema:
type: object
properties:
subAccount:
$ref: '#/components/schemas/SubAccount'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'

/api/v1/sub-accounts/{id}:
delete:
tags: [sub-accounts]
operationId: revokeSubAccount
summary: Revoke a sub-account
description: |
Soft-revokes a sub-account by setting its status to REVOKED. Only the
parent who owns the sub-account can revoke it. Revocation takes effect
immediately on the next request. The audit trail is preserved.
security:
- BearerAuth: []
parameters:
- in: path
name: id
required: true
schema:
type: string
format: uuid
description: Sub-account ID
responses:
'200':
description: Sub-account revoked
content:
application/json:
schema:
type: object
properties:
subAccount:
$ref: '#/components/schemas/SubAccount'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'

components:
securitySchemes:
BearerAuth:
Expand Down Expand Up @@ -3230,6 +3377,13 @@ components:
protocolName:
type: string
nullable: true
actingAsUserId:
type: string
format: uuid
nullable: true
description: >
Non-null when the action was performed by a parent on behalf
of a child via a sub-account delegation.

# ── Recurring Deposit ──────────────────────────────────────────────────
RecurringDepositPlan:
Expand Down Expand Up @@ -3345,6 +3499,43 @@ components:
items: {}
- type: object

# ── Sub-Accounts ─────────────────────────────────────────────────────────
SubAccountPermission:
type: string
enum: [VIEW, DEPOSIT, WITHDRAW, MANAGE_STRATEGY]
description: >
Granular permission for sub-account delegation. Permissions are
independent flags — having DEPOSIT does not implicitly grant VIEW.
SubAccountStatus:
type: string
enum: [ACTIVE, REVOKED]
description: ACTIVE means the sub-account is live; REVOKED means it was soft-deleted.
SubAccount:
type: object
properties:
id:
type: string
format: uuid
parentUserId:
type: string
format: uuid
childUserId:
type: string
format: uuid
permissions:
type: array
items:
$ref: '#/components/schemas/SubAccountPermission'
status:
$ref: '#/components/schemas/SubAccountStatus'
createdAt:
type: string
format: date-time
revokedAt:
type: string
format: date-time
nullable: true

# ── Fiat ──────────────────────────────────────────────────────────────
FiatDirection:
type: string
Expand Down
48 changes: 48 additions & 0 deletions prisma/migrations/20260728000000_add_sub_accounts/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- CreateEnum
CREATE TYPE "SubAccountPermission" AS ENUM ('VIEW', 'DEPOSIT', 'WITHDRAW', 'MANAGE_STRATEGY');

-- CreateEnum
CREATE TYPE "SubAccountStatus" AS ENUM ('ACTIVE', 'REVOKED');

-- AlterTable: Add actingAsUserId to Transaction
ALTER TABLE "transactions" ADD COLUMN "actingAsUserId" TEXT;

-- AlterTable: Add actingAsUserId to AgentLog
ALTER TABLE "agent_logs" ADD COLUMN "actingAsUserId" TEXT;

-- CreateTable
CREATE TABLE "sub_accounts" (
"id" TEXT NOT NULL,
"parentUserId" TEXT NOT NULL,
"childUserId" TEXT NOT NULL,
"permissions" "SubAccountPermission"[] NOT NULL,
"status" "SubAccountStatus" NOT NULL DEFAULT 'ACTIVE',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revokedAt" TIMESTAMP(3),

CONSTRAINT "sub_accounts_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "sub_accounts_parentUserId_childUserId_key" ON "sub_accounts"("parentUserId", "childUserId");

-- CreateIndex
CREATE INDEX "sub_accounts_parentUserId_idx" ON "sub_accounts"("parentUserId");

-- CreateIndex
CREATE INDEX "sub_accounts_childUserId_idx" ON "sub_accounts"("childUserId");

-- CreateIndex
CREATE INDEX "sub_accounts_status_idx" ON "sub_accounts"("status");

-- CreateIndex
CREATE INDEX "transactions_actingAsUserId_idx" ON "transactions"("actingAsUserId");

-- CreateIndex
CREATE INDEX "agent_logs_actingAsUserId_idx" ON "agent_logs"("actingAsUserId");

-- AddForeignKey
ALTER TABLE "sub_accounts" ADD CONSTRAINT "sub_accounts_parentUserId_fkey" FOREIGN KEY ("parentUserId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "sub_accounts" ADD CONSTRAINT "sub_accounts_childUserId_fkey" FOREIGN KEY ("childUserId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Loading
Loading