Skip to the guide
Towerz

Towerz documentation Solana

Shared payment budgets

Coordinate spending across cooperating workers with limits, expiry and revocation.

PostgresX402BudgetStore lets independent Node workers share one payer budget, durable request IDs and revocable payment policies. It is a server SDK component. It does not move money, retain private keys, or turn a wallet into an on-chain spending-limited account.

Provision a policy

Use the same PostgreSQL database for every worker that can sign for a payer. The caller owns the pg.Pool, its TLS settings and its credentials. Use a finite connection timeout. Do not publish the pool or a database URL in browser code.

import pg from "pg";
import { createTowerzX402Client, SOLANA_MAINNET, USDC_MAINNET } from "@towerz/agent-sdk/x402";
import { PostgresX402BudgetStore } from "@towerz/agent-sdk/x402/postgres";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  connectionTimeoutMillis: 3_000,
  max: 10,
  // Configure verified TLS for your database provider in production.
});

const policy = {
  merchants: [{
    url: "https://your-approved-provider.example/research?id=42",
    network: SOLANA_MAINNET,
    asset: USDC_MAINNET,
    payTo: approvedMerchantAddress,
    feePayer: approvedFacilitatorAddress,
  }],
  perRequestAtomic: 100_000n, // 0.10 USDC
  dailyAtomic: 2_000_000n,   // 2.00 USDC across all workers and policies
  expiresAt: Date.now() + 60 * 60 * 1_000,
  allowMainnet: true,
};
const store = new PostgresX402BudgetStore({
  pool,
  payer: signer.address,
  policyId: "research-policy-v1",
});

// Trusted startup/provisioning, before accepting jobs:
await store.initialize();
await store.configurePolicy(policy);

const client = createTowerzX402Client({
  signer, policy, budgetStore: store, rpcUrl: process.env.SOLANA_RPC_URL!,
  // Optional additional application authorization:
  async authorizePayment(context) {
    await requireActiveJobPermission(context.requestId, context.resource);
  },
});
const result = await client.fetchPaid(policy.merchants[0].url, {
  requestId: "unique-persisted-job-step-id",
});
// A merchant-reported receipt is not independently verified chain settlement.

// An authorized operator can stop future guard checks:
await store.revokePolicy();

// On service shutdown, after its in-flight work has completed:
await pool.end();

The addresses, signer, provider and application permission check in this example must be supplied by the integrator. Example code does not authorize a real payment by itself. pg is installed by the consuming server; the SDK accepts its structural pool interface and does not open another connection pool.

Enforcement and persistence

  • Policies bind the exact URL, network, canonical USDC mint, recipient and trusted facilitator. Mainnet requires explicit provisioning and client opt-in.
  • The database is authoritative for per-request limits, expiry, revocation and the payer-wide daily cap. All policies for one payer must use the same cap. Increasing a client's limit or creating another policy cannot increase it. A smaller client cap remains effective for that request.
  • Provisioning is idempotent for the exact same policy. It cannot overwrite an existing policy, reactivate a revoked policy, or change the payer cap. Revoke an old policy and provision a new ID to change its permitted resources or expiry. Raising the payer cap needs a separately reviewed administrative migration; this SDK deliberately exposes no worker-facing reset/increase API.
  • PostgreSQL transactions and payer-scoped transaction advisory locks serialize reservations, transitions and revocations across workers. Unique database constraints also protect request IDs and resources with unresolved payments.
  • Request IDs remain consumed after completion or release. They must be stored with the application job before requesting payment. A retry uses the same ID and is refused; it must inspect the existing outcome rather than pay again.
  • Only a reservation that has never entered signing can be released. A crash, timeout or ambiguous signed outcome stays charged. Unresolved amounts from earlier UTC days also consume today's capacity until their outcome is resolved. This version intentionally has no automatic uncertain-payment release API.
  • Completed amounts charge the original reservation day. Expiry and UTC-day checks use PostgreSQL time, not a worker-supplied clock or day. Crossing UTC midnight stops an in-flight request before further signing or transmission.
  • The client calls an optional application guard, then the store's built-in guard, before reservation, signing and paid transmission. Store guards run automatically when the store is passed directly as budgetStore. Do not wrap it as an object that omits authorizePayment.
  • State is persisted before invoking a signer or transmitting a signed request. Database failures fail closed. Database exception contents are replaced with a stable error code so connection details do not reach the caller.
  • SQL statements have bounded lock and execution timeouts. The caller must also bound connection acquisition using connectionTimeoutMillis and handle the pool's connection-error events according to the pg documentation.

The database retains public payer/merchant identifiers, request IDs, exact resource URLs, atomic amounts, dates and states. It does not store private keys, signed payment headers, secret wallet material or provider RPC credentials. Exact resource URLs should contain no API keys or personal data in their query strings. Keep database access private, with backups and normal access controls.

Authority boundary

These controls govern cooperating SDK clients and a trusted signing service. Anyone who possesses a raw private key or unrestricted signer can bypass this JavaScript library and sign elsewhere. Anyone with broad write access to the budget database can alter its policies. Protect signer access separately, give workers only the authority they need, and reserve policy provisioning/revocation for authenticated administrative operations. An application authorizer is a callback, not proof that a human approved a particular transaction.

Revocation takes effect on the next database authorization check. It cannot recall a signature or request already delivered to an external signer, provider or facilitator. There is necessarily a short interval between checking a database policy and invoking an external operation. Hard cryptographic spending limits or revocation of already issued authorizations require an appropriate on-chain signer/account design; they are not claimed by this store.

Do not treat a merchant receipt as finalized repayment, earned revenue or an independently confirmed transfer. Chain settlement and work delivery require their own verification before advancing the economic lifecycle.

The budget journal retains spending state, not the merchant's transaction signature. If a process exits after a payment response but before the application persists its work receipt, the charge remains consumed and the application must reconcile the transaction from the wallet or chain history. It must not reset the budget or issue a new payment to recover that missing record. Persist returned receipts promptly; spending protection is not automatic receipt recovery.

Local checks

The ordinary test suite runs local HTTP x402 transaction-construction and actual Ed25519-signing tests for guards at each boundary, plus database-failure tests. No funded signer or external payment endpoint is used.

The PostgreSQL suite requires a disposable PostgreSQL 16 server at the strictly checked local address below. It does not accept a production database URL:

docker run --detach --name towerz-x402-budget-tests \
  -e POSTGRES_USER=towerz -e POSTGRES_PASSWORD=towerz-local-test \
  -e POSTGRES_DB=towerz_x402_test \
  -p 127.0.0.1:15436:5432 postgres:16-bookworm

TOWERZ_TEST_X402_DATABASE_URL=postgres://towerz:towerz-local-test@127.0.0.1:15436/towerz_x402_test \
TOWERZ_TEST_X402_CONTAINER=towerz-x402-budget-tests \
  node --import tsx --test tests/x402-postgres.test.ts

docker rm --force towerz-x402-budget-tests

That suite runs eight independent Node processes against one budget, conflicting worker/policy caps, replay and unresolved-resource checks, UTC rollover, revocation, immutable provisioning, exact payment binding, and a real database restart followed by a fresh connection pool. Without the explicit database environment variable, the database integration test is marked skipped rather than presented as passed. The restart check additionally requires the exact disposable container name shown above.

Continue with Towerz

Build with accountable capital.