tributary-so
tributary
TypeScript

Bringing Web2's subscription simplicity to Web3. Tributary enables any Solana-based business to offer truly automated recurring payments

Last updated Jul 4, 2026
11
Stars
1
Forks
13
Issues
0
Stars/day
Attention Score
46
Language breakdown
No language data available.
โ–ธ Files click to expand
README

Tributary

Automated recurring payments on Solana using token delegation. Web2 subscription UX with Web3 transparency โ€” non-custodial, permissionless, and composable.

CI License: MIT Solana

Table of Contents

- Monorepo Layout - The Two Policy Families - PDAs (Program Derived Addresses) - Payment Types - Fee Model - Referral Program - Composable Policies (Validation + Forward) - Gateway Feature Flags

Overview

Tributary is a single Solana program that enables automated, pull-based recurring payments using SPL token delegation. Users approve a delegate once; authorized gateways then pull payments on schedule without further user intervention โ€” and without ever taking custody of the funds.

The protocol exposes two families of pull-payment policies that share the same scheduling model but differ in execution semantics:

  • PaymentPolicy โ€” direct pull payments (subscriptions, milestones,
pay-as-you-go).
  • ComposablePolicy โ€” programmable pull payments with optional validation
(Lighthouse on-chain assertions) and token forwarding (Meteora DLMM swaps) inserted between the pull and the settlement.

Both reuse the same PolicyType enum, the same UserPayment account, the same PaymentGateway, and the same fee-distribution logic.

Program ID: TRibg8W8zmPHQqWtyAD1rEBRXEdyU13Mu6qX1Sg42tJ

Key Features

  • Non-custodial โ€” funds stay in the user's wallet; the program only pulls via SPL delegation.
  • Permissionless execution โ€” any authorized gateway signer can trigger a due payment.
  • Three policy models โ€” Subscription, Milestone (escrow + release conditions), and Pay-as-you-go (usage-based with period caps).
  • Composable policies โ€” opt-in validation (Lighthouse assertions) and token-transform forwards (Meteora DLMM) during execution.
  • Fee architecture โ€” configurable protocol + gateway fees, net/gross modes, per-gateway custom fees, and a 3-tier referral reward pool.
  • Referral program โ€” 6-character codes scoped per gateway with a 3-level chain split.
  • Emergency pause โ€” global kill switch on the ProgramConfig singleton.
  • x402 / HTTP 402 middleware for deferred micropayments.
  • Action Codes โ€” one-time wallet-less payment codes.
  • Multi-app monorepo โ€” SDK, React SDK, payments client, oclif CLI, Express API, scheduler, checkout, and marketing site.

Tech Stack

| Layer | Technology | | --------------- | ----------------------------------------------------------------------------------- | | Smart Contract | Rust, Anchor 0.31.1, anchor-spl (Token / Associated Token) | | Blockchain | Solana (cluster 2.2.11), program ID TRibg8W8zmPHQqWtyAD1rEBRXEdyU13Mu6qX1Sg42tJ | | SDKs | TypeScript, @coral-xyz/anchor, @solana/web3.js, @solana/spl-token | | Frontend | React 19, Vite 7, Tailwind 4, HeroUI, Wallet Adapter, TanStack Query, Jotai | | API Server | Express 4, Drizzle ORM, PostgreSQL (postgres), Redis, Socket.io, KafkaJS | | Scheduler | Node.js, node-cron, commander | | CLI | oclif 4 | | Validation CPI | Lighthouse (L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95) | | Forward CPI | Meteora DLMM (LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo) | | Docs | MkDocs Material (uv / Python) | | Package Manager | pnpm workspaces (root 10.28.2) | | CI/CD | GitHub Actions, semantic-release (per-package), ghcr.io Docker | | Testing | Jest, Anchor tests, Surfpool mainnet-fork |

Prerequisites

  • Node.js 20.19+ or 22.12+
  • pnpm 9.6.0+ (root uses 10.28.2 via corepack)
  • Rust stable toolchain
  • Anchor 0.31.0 (install via avm)
  • Solana CLI 2.2.11
  • Docker (optional, for API / scheduler images and local DBs)
  • Surfpool (required for integration tests)
[!TIP] > make prep runs avm use 0.31.0 to pin the Anchor version. Run it once before
building or testing the program.

Getting Started

1. Clone the Repository

git clone https://github.com/tributary-so/tributary
cd tributary

2. Install Dependencies

The repo is a pnpm workspace (apps/, packages/, tests, programs/tributary).

corepack enable
pnpm install

3. Build the Workspace

Build the smart contract and all publishable packages:

# Build everything (contract + all packages + all apps + docs)
make build

Or build individually:

anchor build # Rust program -> target/deploy/tributary.so pnpm --filter @tributary-so/sdk build # Core SDK (tsup) pnpm --filter @tributary-so/sdk-react build # React SDK pnpm --filter @tributary-so/sdk-x402 build # x402 middleware pnpm --filter @tributary-so/payments build # Payments client pnpm --filter @tributary-so/cli build # oclif CLI

4. Environment Setup

There is no root .env. Each app has its own configuration:

| App | Env file | Notes | | ---------------- | ---------------------------- | ----------------------------- | | apps/api | apps/api/.env.example | DB, Redis, RPC, Kafka | | apps/scheduler | (env vars / CLI flags) | RPC, gateway keypair | | apps/cli | ~/.config/solana/id.json | Uses Solana CLI config | | apps/app | apps/app/.env.example | Vite frontend vars (VITE_*) | | apps/checkout | apps/checkout/.env.example | Checkout page frontend vars |

Copy the relevant example and fill it in:

cp apps/api/.env.example apps/api/.env

5. Run Tests

All tests (Rust unit tests + every jest integration suite) run against a Surfpool mainnet-fork. The program is deployed on mainnet, so the fork carries real config and token state (USDC, USDT, Meteora pools, โ€ฆ) that the tests seed via Surfpool cheatcodes โ€” there is no separate deploy step (Surfpool auto-deploys the program from target/deploy/ on start).

Start Surfpool in one terminal, then run the whole suite with a single command:

# Terminal 1 โ€” start the fork
surfpool start --legacy-anchor-compatibility --no-tui

(or: make run_surfpool)

Terminal 2 โ€” run everything (Rust + every jest suite); first failure aborts

anchor run surfpool

(or: make test_surfpool)

[!WARNING] > anchor test no longer runs the suite. It builds the program, prints a
redirect message, and exits non-zero. Use anchor run surfpool against a
running Surfpool instance instead. Plain cargo test still works for the
Rust unit tests only.

See Testing for the per-suite matrix.

6. Start a Frontend Dev Server

# Main app (React + Vite) โ€” http://localhost:5173
pnpm --filter @tributary-so/app dev

Landing page โ€” http://localhost:5174

pnpm --filter ./apps/landing dev

Checkout page

pnpm --filter ./apps/checkout dev

API server (Express) โ€” http://localhost:3000

pnpm --filter @tributary-so/api dev

Scheduler (cron worker)

pnpm --filter @tributary-so/scheduler dev

7. Run the Docs Locally

cd apps/docs
make serve   # or: uv run mkdocs serve

Open http://localhost:8000.


Architecture

Monorepo Layout

โ”œโ”€โ”€ programs/tributary/        # Solana program (Rust / Anchor 0.31.1)
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ lib.rs             # 21 instruction entrypoints + security.txt
โ”‚       โ”œโ”€โ”€ constants.rs       # PDA seeds, allowlisted CPI programs
โ”‚       โ”œโ”€โ”€ error.rs           # TributaryError enum
โ”‚       โ”œโ”€โ”€ state/             # Account structs (ProgramConfig, PaymentGateway,
โ”‚       โ”‚                      #   UserPayment, PaymentPolicy, ComposablePolicy,
โ”‚       โ”‚                      #   ValidationPda, ReferralAccount, ...)
โ”‚       โ”œโ”€โ”€ instructions/      # composable/, gateway/, payment/, referral/, user/
โ”‚       โ”œโ”€โ”€ policies/          # Strategy trait + Subscription/Milestone/PayAsYouGo
โ”‚       โ”œโ”€โ”€ shared/            # fees, delegation, mint, validation, referral, schedule
โ”‚       โ””โ”€โ”€ utils.rs
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ sdk/                   # @tributary-so/sdk โ€” core TS SDK (tsup)
โ”‚   โ”‚   โ””โ”€โ”€ src/
โ”‚   โ”‚       โ”œโ”€โ”€ sdk.ts         # Tributary class โ€” all instruction builders
โ”‚   โ”‚       โ”œโ”€โ”€ lighthouse.ts  # Fluent assertion facade (Lighthouse wrapper)
โ”‚   โ”‚       โ”œโ”€โ”€ pda.ts         # PDA derivation helpers
โ”‚   โ”‚       โ”œโ”€โ”€ token.ts       # SPL token utilities
โ”‚   โ”‚       โ”œโ”€โ”€ constants.ts   # PROTOCOLFEEBPS, SEEDS, GATEWAY_FEATURES
โ”‚   โ”‚       โ”œโ”€โ”€ types.ts       # Shared TS types
โ”‚   โ”‚       โ””โ”€โ”€ utils.ts
โ”‚   โ”œโ”€โ”€ sdk-react/             # @tributary-so/sdk-react โ€” hooks + UI components
โ”‚   โ”œโ”€โ”€ sdk-x402/              # @tributary-so/sdk-x402 โ€” HTTP 402 Express middleware
โ”‚   โ”œโ”€โ”€ payments/              # @tributary-so/payments โ€” high-level payments client (JWT)
โ”‚   โ””โ”€โ”€ lighthouse/            # lighthouse-sdk-legacy (vendored, private) โ€” assertion client
โ”œโ”€โ”€ apps/
โ”‚   โ”œโ”€โ”€ app/                   # @tributary-so/app โ€” React 19 dashboard
โ”‚   โ”œโ”€โ”€ checkout/              # Checkout page (pay-page.tsx)
โ”‚   โ”œโ”€โ”€ landing/               # Marketing site (React + Vite)
โ”‚   โ”œโ”€โ”€ lando/                 # Lando page
โ”‚   โ”œโ”€โ”€ api/                   # @tributary-so/api โ€” Express + Drizzle + Postgres
โ”‚   โ”‚   โ””โ”€โ”€ src/{routes,services,middleware,db,types}
โ”‚   โ”œโ”€โ”€ scheduler/             # @tributary-so/scheduler โ€” node-cron executor
โ”‚   โ”œโ”€โ”€ cli/                   # @tributary-so/cli โ€” oclif CLI (wallet/gateway/subscription/...)
โ”‚   โ”œโ”€โ”€ showcase-payments/     # Integration showcase
โ”‚   โ”œโ”€โ”€ showcase-topup-sol/   # Auto top-up (composable policy) showcase
โ”‚   โ”œโ”€โ”€ showcase-payment-policies/  # Owner-direct policy creation showcase
โ”‚   โ””โ”€โ”€ docs/                  # MkDocs Material site (uv / Python)
โ”‚       โ””โ”€โ”€ adr/               # Architecture Decision Records (0001-0013)
โ”œโ”€โ”€ tests/                     # Jest integration suite (Surfpool-backed)
โ”œโ”€โ”€ specs/                     # Feature specifications
โ”œโ”€โ”€ branding/                  # Brand assets
โ”œโ”€โ”€ reports/                   # Audit / security finding write-ups
โ”œโ”€โ”€ CONTEXT.md                 # Domain glossary / ubiquitous language
โ””โ”€โ”€ .github/workflows/         # CI pipelines
Orientation for new contributors: CONTEXT.md defines
the ubiquitous language (read it first). apps/docs/adr/
captures the why behind every locked-in architectural decision โ€” 0001-0006
are v1 PaymentPolicy era, 0007-0013 are v2 ComposablePolicy era. Code is the
authority on current state; ADRs are the authority on rationale.

The Two Policy Families

Tributary exposes two policy namespaces that share the scheduling engine:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                       โ”‚           UserPayment                โ”‚
                       โ”‚   ["user_payment", owner, mint]      โ”‚
                       โ”‚   createdpoliciescount โ”€โ”€โ”         โ”‚
                       โ”‚   createdcomposablecount โ”ค         โ”‚
                       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                                    โ”‚
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                โ”‚                                                                             โ”‚
        PaymentPolicy PDA                                              ComposablePolicy PDA
   ["paymentpolicy", userpayment, id]                         ["composablepolicy", userpayment, id]
   id = createdpoliciescount                                   id = createdcomposablecount
                โ”‚                                                             โ”‚
        executepayment                                               executecomposable
        (single CPI: user โ†’ recipient)                  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                                       โ”‚                                             โ”‚
                                              Phase 1: PULL                              Phase 2/3 (optional)
                                              usertoken โ”€โ”€โ–บ intermediateinput_ata       VALIDATE  +  FORWARD
                                                                                             (Lighthouse) (Meteora DLMM)
                                                                                                   โ”‚
                                                                                          settle: recipient + fees
[!IMPORTANT] > PaymentPolicy IDs and ComposablePolicy IDs come from independent
counters on UserPayment (createdpoliciescount vs
createdcomposablecount). A regular policy #1 and a composable policy #1
can coexist on the same UserPayment.

PDAs (Program Derived Addresses)

| PDA | Seeds | Purpose | | ------------------ | ------------------------------------------------ | ----------------------------------------------------------------------- | | ProgramConfig | ["config"] | Singleton โ€” protocol admin, protocol fee, emergency pause | | PaymentGateway | ["gateway", authority] | Per-authority gateway settings (fees, signer, flags, referral config) | | UserPayment | ["user_payment", owner, mint] | Per user+mint; the delegate for token pulls; holds both counters | | PaymentPolicy | ["paymentpolicy", userpayment, policy_id] | Regular pull-payment policy (Subscription / Milestone / PayAsYouGo) | | ComposablePolicy | ["composablepolicy", userpayment, policy_id] | Programmable pull-payment policy (validation + forward hooks) | | ValidationPda | ["composablevalidation", composablepolicy] | Stores Lighthouse assertion data (โ‰ค 1024 bytes) | | ReferralAccount | ["referral", gateway, referral_code] | 6-char referral code + chain relationships (gateway-scoped) | | PaymentsDelegate | ["payments"] | Legacy global delegate (deprecated โ€” UserPayment PDA is the delegate) |

Payment Types

All three variants are part of a single PolicyType enum and are **exactly 128 bytes each** (fixed-size for account stability). Both PaymentPolicy and ComposablePolicy reuse this enum.

Subscription

Fixed recurring payments at regular intervals.

PolicyType::Subscription {
    amount: u64,                         // payment per cycle
    autorenew: bool,                    // continue past maxrenewals?
    max_renewals: Option<u32>,           // ceiling (None = indefinite)
    payment_frequency: PaymentFrequency, // Daily/Weekly/Monthly/Quarterly/.../Custom(secs)
    nextpaymentdue: i64,               // gates execution
    padding: [u8; 97],
}

PaymentFrequency supports Daily, Weekly, Monthly, Quarterly, SemiAnnually, Annually, and Custom(u64) (arbitrary seconds).

Milestone

Project-based compensation with up to 4 escrowed milestones released via a release-condition bitmap.

PolicyType::Milestone {
    milestone_amounts: [u64; 4],
    milestone_timestamps: [i64; 4],
    current_milestone: u8,
    release_condition: u8,   // bit0=due-date, bit1=gateway, bit2=owner, bit3=recipient
    total_milestones: u8,    // 1..=4
    escrow_amount: u64,
    padding: [u8; 53],
}

Release bits (bits 1โ€“3 are mutually exclusive):

| Bit | Mask | Condition | | --- | -------- | --------------------------- | | 0 | 0b0001 | Milestone due-date reached | | 1 | 0b0010 | Gateway authority must sign | | 2 | 0b0100 | Policy owner must sign | | 3 | 0b1000 | Recipient must sign |

Pay-as-you-go

Usage-based billing with per-period caps and per-call chunk limits.

PolicyType::PayAsYouGo {
    maxamountper_period: u64,
    maxchunkamount: u64,
    periodlengthseconds: u64,
    currentperiodstart: i64,
    currentperiodtotal: u64,   // auto-resets when period elapses
    padding: [u8; 88],
}

Fee Model

Fees are computed in programs/tributary/src/shared/fees.rs:

  • Protocol fee โ€” default 100 bps (1%), stored on ProgramConfig.protocolfeebps.
  • Gateway fee โ€” configurable gatewayfeebps (0..10000), stored per-gateway.
  • Custom protocol fee โ€” per-gateway override (bit 2 of feature_flags).
  • Combined guard โ€” gatewayfeebps + protocolfeebps < 10000 is enforced (recipient must receive > 0).
Two amount modes (PaymentGateway.feature_flags bit 1):
  • Gross (default) โ€” recipient = paymentamount โˆ’ gatewayfee โˆ’ protocol_fee.
  • Net โ€” recipient receives exactly payment_amount; fees are added on top and pulled from the user.
Math: (amount * bps) / 10000 (rounds down; dust goes to protocol).

Referral Program

Gateways opt into a 3-tier referral reward pool (bit 0 of feature_flags):

  • referralallocationbps โ€” fraction of the gateway fee carved into the pool (0..=2500, i.e. up to 25%).
  • referraltiersbps โ€” 3-element split of the pool across [direct, level2, level3], must sum to 10000.
Referral codes are 6-byte arrays, scoped per gateway via the ReferralAccount PDA (["referral", gateway, referral_code]). The chain is traversed at payment time to distribute rewards.

Composable Policies (Validation + Forward)

A ComposablePolicy runs two optional hooks during execution, between the pull and settlement. Both are opt-in via sentinels.

Execution flow (3 phases)

execute_composable:
  โ”Œโ”€ Phase 1: PULL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ UserPayment PDA signs:                                          โ”‚
  โ”‚   usertokenaccount โ”€โ”€โ–บ intermediateinputata                 โ”‚
  โ”‚ (intermediate ATAs owned by ComposablePolicy PDA โ€” NOT the      โ”‚
  โ”‚  UserPayment PDA, decoupling intermediate authority from the    โ”‚
  โ”‚  user-source delegate)                                          โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  โ”Œโ”€ Phase 2: VALIDATE (optional) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ CPI into validation_program (Lighthouse) with stored            โ”‚
  โ”‚ validation_data + declared read-accounts. Veto on assertion     โ”‚
  โ”‚ failure. Read-only โ€” cannot move funds.                         โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  โ”Œโ”€ Phase 3: FORWARD (optional) + SETTLE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ If forward enabled: CPI into target_program (Meteora DLMM) to   โ”‚
  โ”‚   swap intermediateinput โ”€โ”€โ–บ intermediateoutput.              โ”‚
  โ”‚ ByteRangeCheck pins the forward instruction selector.           โ”‚
  โ”‚ Sweep intermediate_output โ”€โ”€โ–บ recipient + protocol + gateway.   โ”‚
  โ”‚ minoutputamount enforced on NET (post-fee) amount.            โ”‚
  โ”‚ If forward disabled (sentinel): sweep input directly to         โ”‚
  โ”‚   recipient + fees (same-mint topup pattern).                   โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ForwardConfig

struct ForwardConfig {
    target_program: Pubkey,             // Pubkey::default() = disabled (sentinel)
    inputmint: Pubkey,                 // == userpayment.token_mint
    output_mint: Pubkey,                // recipient delivery mint
    minoutputamount: Option<u64>,     // NET (post-fee) minimum (DeFi convention)
    forward_flags: u8,
    numdatachecks: u8,
    data_checks: [ByteRangeCheck; 4],   // pin forward instruction discriminator
}
  • targetprogram must be in ALLOWEDFORWARD_PROGRAMS (Meteora DLMM). Pubkey::default() disables the forward.
  • When enabled, โ‰ฅ1 ByteRangeCheck must pin the discriminator at offset 0.
  • minoutputamount is checked against the net amount (after fees).

ValidationConfig

struct ValidationConfig {
    validation_program: Pubkey,   // SystemProgram = disabled (sentinel)
    numvalidationaccounts: u8,  // โ‰ค 10 read-accounts for the assertion
}

Assertion data (โ‰ค 1024 bytes) lives in a separate ValidationPda (["composablevalidation", composablepolicy]). At execute, the program CPIs into the validation program passing this data + declared read-accounts as remaining_accounts.

  • validationprogram must be in ALLOWEDVALIDATION_PROGRAMS (Lighthouse). SystemProgram disables validation.

Allowlists (hard-coded in constants.rs)

| List | Programs | | ----------------------------- | ---------------------------------------------------------- | | ALLOWEDFORWARDPROGRAMS | Meteora DLMM LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo | | ALLOWEDVALIDATIONPROGRAMS | Lighthouse L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95 |

Building Lighthouse assertions (SDK facade)

The SDK ships a fluent lighthouse facade over the vendored packages/lighthouse client. Never hand-roll the serialization.

import { lighthouse, LIGHTHOUSEPROGRAMID } from "@tributary-so/sdk";

// Assert hotWallet USDC balance < 50 USDC before topping up const guard = lighthouse .tokenAccount(hotWalletUsdcAta) .amount(50000000, "<") .build();

// guard.data โ†’ Buffer (stored in ValidationPda) // guard.numAccounts โ†’ 1 (numValidationAccounts) // guard.accounts โ†’ [hotWalletUsdcAta] (Lighthouse read-accounts)

Covers tokenAccount, mintAccount, accountInfo, accountData, accountDelta, sysvarClock, stakeAccount, merkleTree, plus operator sugar ("<", ">=", "!=", "in", โ€ฆ).

[!NOTE]
The facade owns only the Lighthouse target accounts. The caller assembles
Tributary's full remaining_accounts list ([ValidationPda, ...guard.accounts]).

Gateway Feature Flags

PaymentGateway.feature_flags is a bit-vector (see constants.rs):

| Bit | Mask | Flag | Effect | | --- | ---- | ----------------------------- | -------------------------------------------------------------- | | 0 | 0x01 | FEATURE_REFERRAL | Enables the referral reward pool | | 1 | 0x02 | FEATURENETAMOUNT | Recipient receives exactly payment_amount; fees added on top | | 2 | 0x04 | FEATURECUSTOMPROTOCOLFEE | Overrides default protocol fee with customprotocolfeebps |


Workspaces Reference

| Package | Path | Version | Purpose | | ------------------------- | --------------------- | ------- | ------------------------------------------------ | | @tributary-so/contract | programs/tributary | 1.6.1 | Rust Anchor program (publishes IDL) | | @tributary-so/sdk | packages/sdk | 1.11.1 | Core TypeScript SDK โ€” instruction builders, PDAs | | @tributary-so/sdk-react | packages/sdk-react | 1.6.0 | React hooks + UI components (SubscriptionButton) | | @tributary-so/sdk-x402 | packages/sdk-x402 | 1.5.0 | Express 5 HTTP 402 middleware + metering | | @tributary-so/payments | packages/payments | 1.9.1 | High-level payments client with JWT verification | | lighthouse-sdk-legacy | packages/lighthouse | 2.0.1 | Vendored Lighthouse assertion client (private) | | @tributary-so/cli | apps/cli | 1.8.0 | oclif CLI (tributary binary) | | @tributary-so/api | apps/api | 1.9.0 | Express + Drizzle + Postgres + Redis API server | | @tributary-so/scheduler | apps/scheduler | 1.5.2 | node-cron payment executor | | @tributary-so/app | apps/app | 1.14.0 | React 19 dashboard | | (unpublished) | apps/checkout | โ€” | Checkout pay-page | | (unpublished) | apps/landing | โ€” | Marketing site | | (unpublished) | apps/lando | โ€” | Lando page | | (unpublished) | apps/docs | โ€” | MkDocs documentation |


SDK Usage

Install

pnpm add @tributary-so/sdk

optional companions:

pnpm add @tributary-so/sdk-react # React hooks + buttons pnpm add @tributary-so/sdk-x402 # HTTP 402 middleware pnpm add @tributary-so/payments # high-level payments client

Create a subscription

import { Tributary, getPaymentFrequency, encodeMemo } from "@tributary-so/sdk";
import { Connection, PublicKey } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com"); const PROGRAM_ID = new PublicKey("TRibg8W8zmPHQqWtyAD1rEBRXEdyU13Mu6qX1Sg42tJ");

const sdk = new Tributary(PROGRAM_ID, connection);

// Full setup: ATA + user payment + policy + delegate approval const ixs = await sdk.createSubscription( tokenMint, // e.g. USDC mint recipient, // recipient wallet gateway, // gateway PDA authority new BN("1000000"), // amount (6 decimals = 1 USDC) true, // auto-renew 12, // max renewals getPaymentFrequency("monthly"), encodeMemo("Pro plan") );

Execute a payment (permissionless โ€” called by gateway signer)

const execIx = await sdk.executePayment(paymentPolicyPda);

Create a composable policy with a Lighthouse guard

import { lighthouse, LIGHTHOUSEPROGRAMID } from "@tributary-so/sdk";

const guard = lighthouse .tokenAccount(hotWalletUsdcAta) .amount(50000000, "<") .build();

const ix = await sdk.getCreateComposablePolicyInstruction( tokenMint, recipient, gateway, policyType, // same PolicyType enum "Auto topup guard", forwardConfig, // targetProgram = PublicKey.default for no-swap topup LIGHTHOUSEPROGRAMID, // SystemProgram = no validation guard.numAccounts, guard.data );

const execIx = await sdk.executeComposable( composablePolicyPda, instructionData, // forward ix data (empty if disabled) forwardAmount ?? null, remainingAccounts // [ValidationPda, ...lighthouseTargets, ...forwardAccts] );

React โ€” SubscriptionButton

import { SubscriptionButton, PaymentInterval } from "@tributary-so/sdk-react";

<SubscriptionButton amount={new BN("10000000")} // 10 USDC token={USDC_MINT} recipient={recipientWallet} gateway={gatewayAddress} interval={PaymentInterval.Monthly} maxRenewals={12} memo="Monthly donation" label="Subscribe for $10/month" />;

x402 HTTP 402 middleware

import { createX402Middleware } from "@tributary-so/sdk-x402";

const middleware = createX402Middleware({ scheme: "deferred", network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", amount: 100, recipient: process.env.RECIPIENT_WALLET!, gateway: process.env.GATEWAY!, tokenMint: process.env.TOKEN_MINT!, paymentFrequency: "monthly", jwtSecret: process.env.JWT_SECRET!, sdk, connection, });

app.use("/api/premium", middleware);


CLI Manager

The oclif CLI (apps/cli, binary tributary) exposes topics for every program operation:

pnpm --filter @tributary-so/cli build

Topics:

tributary wallet # keypair create / import / balance tributary program # protocol init + config queries tributary user # UserPayment create / list / inspect tributary gateway # gateway create / configure / inspect tributary subscription # policy create / list / pause / resume / delete tributary payments # trigger execution tributary referral # referral accounts + chain queries tributary pda # derive any PDA
[!TIP]
The SDK also ships a low-level manager: pnpm --filter @tributary-so/sdk manager.

Environment Variables

Required (program interaction)

| Variable | Description | Example | | ---------------- | -------------------- | --------------------------------------------- | | SOLANARPCURL | Solana RPC endpoint | https://api.mainnet-beta.solana.com | | PROGRAM_ID | Tributary program ID | TRibg8W8zmPHQqWtyAD1rEBRXEdyU13Mu6qX1Sg42tJ |

Optional (Anchor / deployment)

| Variable | Description | Default | | --------------------- | -------------------------------------- | ------------------------------------- | | ANCHOR_WALLET | Path to Solana wallet keypair | ~/.config/solana/id.json | | ANCHORPROVIDERURL | Anchor provider URL | localnet | | SOLANA_API | Override Makefile RPC (deploy scripts) | https://api.mainnet-beta.solana.com | | BUFFER | Buffer path for buffer-based deploys | โ€” |

App-specific

Each app documents its own variables in its .env.example. See Getting Started ยง4.

[!IMPORTANT]
Never commit .env files. The repo only ships .env.example templates.

Available Scripts

Root (package.json + Makefile)

| Command | Description | | --------------------- | ---------------------------------------------------------------------------- | | pnpm install | Install all workspace dependencies | | pnpm run lint | Lint all workspaces | | pnpm run lint:fix | Auto-fix lint issues | | make prep | Pin Anchor (avm use 0.31.0) | | make build | Build contract + all packages + all apps + docs | | make run_surfpool | Start Surfpool mainnet-fork (--legacy-anchor-compatibility) | | make test_surfpool | Run the full suite against Surfpool (anchor run surfpool) | | make test | anchor test โ€” prints a redirect message (use make test_surfpool instead) | | anchor run surfpool | Full suite (Rust + every jest suite) against a running Surfpool instance |

Program (programs/tributary)

| Command | Description | | -------------- | -------------------------------------------------- | | anchor build | Build .so + IDL โ†’ target/deploy/ | | anchor test | No-op โ€” prints a redirect to anchor run surfpool | | cargo test | Rust unit tests only |

Deployment (Makefile)

| Command | Description | | ----------------------------- | ------------------------------------------------------ | | make devnet_build | Build for devnet | | make devnet_deploy | Deploy to devnet (--program-keypair) | | make devnetdeploybuffer | Buffer-based devnet deploy (BUFFER=...) | | make mainnet_build | Build with --features mainnet | | make mainnet_deploy | Direct mainnet deploy (--upgradeable) | | make mainnetdeploybuffer | Buffer-based mainnet deploy (recommended for upgrades) | | make mainnet_expand | Extend program account size by 20480 bytes | | make publish_idl | Upgrade on-chain IDL | | make verifiable_build | solana-verify build + hash + deploy + verify | | make submit-verifable_build | Remote verifiable build via solana-verify |

Packages

| Command | Description | | --------------------------------------------- | ---------------------- | | pnpm --filter @tributary-so/sdk build | Build core SDK (tsup) | | pnpm --filter @tributary-so/sdk-react build | Build React SDK (tsup) | | pnpm --filter @tributary-so/sdk-x402 build | Build x402 middleware | | pnpm --filter @tributary-so/payments build | Build payments client | | pnpm --filter @tributary-so/cli build | Build oclif CLI | | pnpm --filter @tributary-so/sdk manager | Run SDK manager REPL |

Apps

| Command | Description | | ------------------------------------------- | ------------------------------ | | pnpm --filter @tributary-so/app dev | Vite dev server (dashboard) | | pnpm --filter ./apps/landing dev | Vite dev server (marketing) | | pnpm --filter ./apps/checkout dev | Vite dev server (checkout) | | pnpm --filter @tributary-so/api dev | Express dev server (tsx watch) | | pnpm --filter @tributary-so/scheduler dev | Scheduler worker |


Testing

Test matrix

Every suite runs against a Surfpool mainnet-fork (the program is deployed on mainnet, so the fork supplies real config / token / pool state). Rust unit tests are cluster-agnostic.

| Suite | Command | Runner | Requires | | ------------------------------ | ----------------------------- | ------ | -------- | | Rust unit tests | anchor run test-cargo | cargo | Rust | | Integration (regular policies) | anchor run test-integration | jest | Surfpool | | Composable policies | anchor run test-composable | jest | Surfpool | | Surfpool harness | anchor run test-surfpool | jest | Surfpool | | Surfpool topup (no swap) | anchor run test-topup | jest | Surfpool | | Surfpool topup (with swap) | anchor run test-topup-swap | jest | Surfpool | | Surfpool topup (SOL) | anchor run test-topup-sol | jest | Surfpool | | Everything | anchor run surfpool | all | Surfpool |

Running the full suite

# Terminal 1 โ€” start the Surfpool mainnet-fork
make run_surfpool

Terminal 2 โ€” run the whole suite (first failure aborts)

anchor run surfpool

anchor run surfpool is wired in Anchor.toml to chain every suite in order:

test-cargo       โ†’ cargo test
test-integration โ†’ jest ./tests/tributary.test.ts
test-composable  โ†’ jest ./tests/composable.test.ts
test-surfpool    โ†’ jest ./tests/surfpool.test.ts
test-topup       โ†’ jest ./tests/topup-balance.test.ts
test-topup-swap  โ†’ jest ./tests/topup-balance-swap.test.ts
test-topup-sol   โ†’ jest ./tests/topup-balance-sol.test.ts
[!IMPORTANT] > anchor test is intentionally a no-op that prints a redirect message and
exits non-zero. The suite must be driven via anchor run surfpool (or
make test_surfpool) against a running Surfpool instance, because Surfpool
auto-deploys the program from target/deploy/ and the tests rely on the
forked mainnet state (USDC/USDT mints, the existing ProgramConfig, โ€ฆ).

Test files

tests/
โ”œโ”€โ”€ tributary.test.ts          # Full PaymentPolicy flow (init โ†’ execute)
โ”œโ”€โ”€ composable.test.ts         # ComposablePolicy: validation + forward
โ”œโ”€โ”€ topup-balance.test.ts      # Same-mint topup (no forward)
โ”œโ”€โ”€ topup-balance-swap.test.ts # Topup with Meteora DLMM swap
โ”œโ”€โ”€ surfpool.test.ts           # Surfpool harness
โ”œโ”€โ”€ constants.ts               # Shared test pubkeys (METEORA_DLMM, LIGHTHOUSE)
โ”œโ”€โ”€ surfpool-helpers.ts
โ””โ”€โ”€ helpers/

Writing tests

Mirror the source structure with a .test.ts suffix. Integration tests use the Surfpool-backed Anchor provider. Prefer accountsStrict() over accounts() for type safety.


Deployment

Smart Contract

The program deploys upgradeable (Anchor.toml โ†’ upgradeable = true). Use the Makefile targets which bake in the correct keypairs and RPC.

Devnet

make prep
make devnet_build
make devnet_deploy                       # direct

or buffer-based (recommended for upgrades):

BUFFER=<path> make devnetdeploybuffer

Mainnet

make prep
make mainnet_build                       # builds with --features mainnet
make mainnetdeploybuffer               # buffer + program-id deploy (recommended)

or direct:

make mainnet_deploy

Verifiable builds (recommended)

Deterministic Docker builds verified on-chain via solana-verify:

make verifiable_build                    # build + hash + deploy + verify

or submit a remote verification job:

make submit-verifable_build

See DEPLOYMENT.md for the manual verifiable-build runbook.

Extending program size

make mainnet_expand                      # +20480 bytes

devnet:

make devnet_expand

Publishing the IDL

make publish_idl

Docker (API + Scheduler)

Both apps/api and apps/scheduler ship multi-stage Dockerfiles built in CI and pushed to ghcr.io/tributary-so/....

# API
docker build -t tributary-api -f apps/api/Dockerfile .

Scheduler

docker build -t tributary-scheduler -f apps/scheduler/Dockerfile .

Run locally:

docker run --rm \
  -e DATABASE_URL=postgresql://... \
  -e SOLANARPCURL=https://api.mainnet-beta.solana.com \
  -p 3000:3000 \
  tributary-api

docker run --rm \ -e SOLANARPCURL=https://api.mainnet-beta.solana.com \ -e ANCHOR_WALLET=/keys/gateway.json \ -v ~/.config/solana:/keys:ro \ tributary-scheduler

SDK Packages

All publishable packages use semantic-release (gitmoji-flavored) per-package via semantic-release-monorepo. Releases trigger automatically on pushes to main (see .github/workflows/semantic-release.yml).

# Manual dry-run for a single package
pnpm --filter @tributary-so/sdk release --dry-run

Frontend Apps

The app, landing, checkout, and lando pages deploy automatically via GitHub Actions (.github/workflows/app-prod.yaml, landing-page.yaml, checkout-page.yaml, lando-page.yaml).

CI/CD

The main pipeline (.github/workflows/main.yaml) is change-detected:

  • Detect changed packages โ€” diff against the previous tag.
  • Test SDKs โ€” runs per changed package path.
  • Release โ€” semantic-release per package.
  • Deploy โ€” app, landing, checkout, lando, docs, typedocs (conditional).
  • Docker โ€” build & push API + scheduler images to ghcr.io (conditional).
Trigger a full post-release rebuild without cutting a release:
gh workflow run main.yaml -f post_release=true

Troubleshooting

Surfpool / integration tests fail

[!WARNING]
All jest suites require a running Surfpool mainnet-fork. anchor test
no longer runs them โ€” it prints a redirect message and exits. Use
anchor run surfpool instead.
# Start Surfpool first (separate terminal)
make run_surfpool

Then run the whole suite (Rust + every jest suite)

anchor run surfpool

If Surfpool is stuck, restart it:

surfpool start --legacy-anchor-compatibility --no-tui
[!TIP]
Surfpool persists fork overrides between runs. If a test fails because an
account (e.g. ProgramConfig) is in an unexpected state from a prior run,
restart Surfpool to get back to a pristine mainnet fork, then re-run
anchor run surfpool.

executepayment / executecomposable fails

| Symptom | Likely cause | Fix | | ----------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- | | MissingDelegate / insufficient delegate | User has not approved the UserPayment PDA as delegate | Approve delegate on the user token account with sufficient amount | | PaymentNotDue | nextpaymentdue is in the future (Subscription) | Wait for the due timestamp, or warp time in tests | | EmergencyPauseActive | ProgramConfig.emergency_pause == true | Admin must clear the flag | | PolicyPaused | Policy status != Active | changepaymentpolicy_status โ†’ Active | | CombinedFeeBpsExceedsMax | gatewayfeebps + protocolfeebps >= 10000 | Lower the gateway fee |

Anchor build fails

# Pin the toolchain
make prep

Clear caches

rm -rf ~/.anchor target anchor build

SDK build fails

rm -rf node_modules pnpm-lock.yaml
pnpm install
pnpm --filter @tributary-so/sdk build

Program deployment fails

solana balance                          # ensure SOL for rent + fees
solana config get                       # verify cluster + keypair

If "account already in use" on program-id:

BUFFER=/tmp/buffer.json make mainnetdeploybuffer

Composable: forward CPI rejected

  • targetprogram must be in ALLOWEDFORWARD_PROGRAMS (Meteora DLMM) or Pubkey::default() to disable.
  • โ‰ฅ1 ByteRangeCheck must pin the instruction discriminator at offset 0.
  • minoutputamount is checked against the net (post-fee) output.
  • Intermediate ATAs are owned by the ComposablePolicy PDA, not the UserPayment PDA.

Composable: validation CPI

[!CAUTION]
The validation CPI dispatcher (shared/validation.rs) is currently a no-op
stub (Ok(())). It is wired but does not invoke Lighthouse at runtime yet.
The Lighthouse SDK facade, ValidationPda storage, and account splitting are
all implemented and tested; the on-chain dispatch is tracked as a follow-up.
Treat validation as "configured, not enforced" until the dispatch lands.

TypeScript type errors

pnpm run lint
pnpm run lint:fix

Security

  • Non-custodial โ€” funds remain in user wallets; only SPL delegation is used.
  • Emergency pause โ€” ProgramConfig.emergency_pause blocks all execution.
  • Access control โ€” authority verification on every mutating instruction.
  • CPI allowlists โ€” forward (Meteora DLMM) and validation (Lighthouse) target programs are hard-coded.
  • CPI signer sanitization โ€” validation & forward builders do not forward issigner from remainingaccounts (closes a privilege pass-through vector).
  • Intermediate ATA ownership โ€” owned by the ComposablePolicy PDA, isolating transient balances from user source funds.
  • security.txt โ€” embedded on-chain via solana-security-txt.
  • Audits โ€” see AUDITS.md and SECURITY.md. Findings are documented in reports/.
Report vulnerabilities to security@tributary.so per the policy in SECURITY.md.

Contributing

  • Fork the repository.
  • Create a feature branch: git checkout -b feature/your-feature.
  • Write tests first (TDD) โ€” mirror source structure with .test.ts.
  • Run the suite: make runsurfpool (start the fork), then make testsurfpool (or anchor run surfpool).
  • Lint: pnpm run lint (must be clean).
  • Commit with conventional commits (gitmoji โ€” releases are automated).
  • Open a pull request.
[!IMPORTANT]
This repo uses beans for issue tracking.
Reference relevant bean IDs in commit messages.

Conventions

  • Rust: snake_case files, Result<()> error handling, #[account] fixed sizes with padding.
  • TypeScript: strict types, PublicKey for addresses, anchor.BN for big numbers, accountsStrict().
  • Imports: Solana first, then Anchor, then local modules.
  • PDAs: always derive via packages/sdk/src/pda.ts helpers.

License

MIT โ€” see LICENSE.

Community & Support

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท tributary-so/tributary ยท Updated daily from GitHub