Relay TypeScript SDK
Relay is PBJ Protocol's production TypeScript SDK for USDC milestone escrow on Solana. It helps applications, agencies, clients, and independent teams define deliverables, secure project budgets before work begins, preserve delivery evidence, approve releases, and resolve disputes through explicit rules.
- Typed end to end — strict TypeScript models, errors, and service APIs
- Non-custodial signing — Relay prepares transactions; your wallet signs them
- Devnet and mainnet — explicit network selection with mainnet safeguards
- Evidence ready — GitHub, Figma, Google Drive, URLs, and checksums
- Safe transport — timeouts, abort signals, idempotency, retry controls, and redaction
[!WARNING] Relay enforces funding, evidence, approval, and release rules. It cannot independently determine whether subjective creative or software work meets expectations. Define measurable milestone criteria and select a dispute resolver before funding.
Install
npm i @pbjpro/relay
Current version: v1.3.0
Requirements
- Node.js 20 or newer, or a modern browser with
fetchandAbortController - A Relay API key for authenticated server operations
- A compatible Solana wallet for signing funding and release transactions
- Decimal strings for all token amounts—never JavaScript floating-point numbers
The package includes ESM, CommonJS, declaration files, and source maps.
Quickstart
import { RelayClient } from "@pbjpro/relay";
const relay = new RelayClient({
apiKey: process.env.RELAY_API_KEY!,
baseUrl: process.env.RELAY_API_URL,
network: "devnet",
});
Keep API keys on trusted servers. Do not include privileged Relay or integration credentials in browser bundles.
Environment
RELAY_API_KEY=relay_test_replace_me
RELAY_API_URL=https://api.0xpbj.pro
RELAY_NETWORK=devnet
RELAY_CONFIRM_MAINNET=false
See .env.example for the complete configuration template.
Create an escrow project
const project = await relay.escrows.create({
name: "Brand redesign",
clientWallet: "CLIENT_SOLANA_ADDRESS",
freelancerWallet: "FREELANCER_SOLANA_ADDRESS",
resolverWallet: "RESOLVER_SOLANA_ADDRESS",
currency: "USDC",
milestones: [
{
title: "Creative direction",
amount: "1500.00",
description: "Approved research and creative direction",
dueAt: "2027-01-31T00:00:00.000Z",
},
{
title: "Final delivery",
amount: "3500.00",
description: "Approved final files and documented handoff",
dueAt: "2027-02-28T00:00:00.000Z",
},
],
disputePolicy: {
resolverType: "human",
inactivityTimeoutHours: 168,
evidenceWindowHours: 72,
},
idempotencyKey: "brand-redesign-001",
});
Fund escrow with USDC
Relay prepares the transaction. The client wallet reviews and signs it locally.
const funding = await relay.escrows.prepareFunding({
projectId: project.id,
amount: "5000.00",
});
const signature = await wallet.sendTransaction(funding.transaction);
await relay.escrows.confirmFunding({
projectId: project.id,
signature,
});
Relay never needs a wallet seed phrase or private key.
Managed funding workflow
The high-level workflow prepares the transaction, delegates signing to your wallet, and confirms the submitted signature. Private keys never enter the SDK.
const confirmation = await relay.escrows.fund({
projectId: project.id,
amount: "5000.00",
signTransaction: (transaction) => wallet.sendTransaction(transaction),
onProgress: ({ stage, signature }) => {
console.log(stage, signature ?? "");
},
});
Submit milestone evidence
Pin specific, immutable versions whenever possible. A live branch, design file, or Drive link may change after submission.
const submission = await relay.milestones.submit({
milestoneId: "milestone_id",
summary: "Final files and handoff documentation",
evidence: [
{
type: "github",
repository: "pbjprotocol/client-project",
commitSha: "FULL_COMMIT_SHA",
},
{
type: "figma",
fileKey: "FIGMA_FILE_KEY",
versionId: "FIGMA_VERSION_ID",
},
{
type: "google_drive",
fileId: "GOOGLE_DRIVE_FILE_ID",
checksum: "SHA256_CHECKSUM",
},
],
idempotencyKey: "delivery-001",
});
Verify delivery evidence
const report = await relay.evidence.verify("evidence_id");
if (!report.valid) {
console.error("Evidence verification failed", report.checks);
}
Approve and release
const prepared = await relay.milestones.prepareRelease({
milestoneId: submission.milestoneId,
});
const signature = await wallet.sendTransaction(prepared.transaction);
await relay.milestones.confirmRelease({
milestoneId: submission.milestoneId,
signature,
});
Signed blockchain transaction confirmations are never automatically retried.
The equivalent managed release workflow is available as relay.milestones.release():
await relay.milestones.release({
milestoneId: submission.milestoneId,
signTransaction: (transaction) => wallet.sendTransaction(transaction),
});
Resolve a dispute
const dispute = await relay.disputes.create({
milestoneId: "milestone_id",
reason: "delivery_does_not_match_scope",
statement: "The submitted package omits the agreed mobile layouts.",
evidenceIds: ["evidence_1", "evidence_2"],
idempotencyKey: "dispute-001",
});
await relay.disputes.resolve({
disputeId: dispute.id,
outcome: "split",
statement: "Desktop scope delivered; mobile scope remains incomplete.",
clientAmount: "1800.00",
freelancerAmount: "1200.00",
});
Webhooks
Always verify the exact, unmodified request body before processing a webhook. Verification includes HMAC authentication and timestamp replay protection.
const event = await relay.webhooks.verify({
payload: rawRequestBody,
signature: request.headers["x-relay-signature"],
secret: process.env.RELAY_WEBHOOK_SECRET!,
});
switch (event.type) {
case "milestone.submitted":
console.log(event.data.submission.id);
break;
case "dispute.opened":
console.log(event.data.id);
break;
}
Use relay.webhooks.idempotent(store, handler) with your database or cache to prevent duplicate delivery processing. Events are recorded only after the handler succeeds.
Devnet and mainnet
Devnet is the default and recommended environment during development.
const relay = new RelayClient({
apiKey: process.env.RELAY_API_KEY!,
network: "devnet",
});
Mainnet requires explicit confirmation and never silently falls back to devnet.
const relay = new RelayClient({
apiKey: process.env.RELAY_API_KEY!,
network: "mainnet",
confirmMainnet: true,
});
Integrations
| Integration | Delivery evidence |
|---|---|
| GitHub | Repositories, immutable commit SHAs, delivery activity |
| Figma | File versions, frozen previews, review comments |
| Google Drive | Files, folders, version metadata, checksums |
Integration tokens should use the narrowest permissions your workflow supports.
Pagination
const page = await relay.milestones.list({
projectId: project.id,
status: ["submitted", "in_dispute"],
page: 1,
limit: 20,
});
for await (const milestone of relay.milestones.iterate({
projectId: project.id,
})) {
console.log(milestone.id, milestone.status);
}
Transaction monitoring
for await (const update of relay.solana.watchTransaction(signature, {
timeoutMs: 60_000,
signal: abortController.signal,
})) {
console.log(update.status);
}
The iterator emits status changes and stops at confirmation. Failed transactions and timeouts produce typed errors.
Typed errors
import { RelayAuthenticationError, RelayTransactionError } from "@pbjpro/relay";
try {
await createFundedProject();
} catch (error) {
if (error instanceof RelayAuthenticationError) {
console.error("Check RELAY_API_KEY");
} else if (error instanceof RelayTransactionError) {
console.error("Transaction failed", error.signature);
} else {
throw error;
}
}
All Relay errors expose code, message, and retryable, plus optional statusCode, requestId, cause, and structured details.
Public services
| Service | Methods |
|---|---|
relay.escrows | create, get, list, prepareFunding, confirmFunding, cancel, getBalance, getActivity |
relay.milestones | create, get, list, iterate, submit, prepareRelease, confirmRelease, requestChanges |
relay.evidence | create, get, list, refresh, getFrozenPreview, verifyChecksum |
relay.disputes | create, get, list, addEvidence, addStatement, resolve, getTimeline |
relay.integrations | GitHub, Figma, and Google Drive evidence services |
relay.solana | Network, RPC, USDC mint, transaction status, confirmation, and wallet validation |
relay.webhooks | HMAC verification, replay protection, duplicate detection, and idempotent handlers |
Testing utilities
The optional testing entry point provides deterministic fixtures and a mock Relay transport without network access.
import { createRelayMock, projectFixture } from "@pbjpro/relay/testing";
const project = projectFixture();
const mock = createRelayMock([
{ path: `/v1/escrows/${project.id}`, body: project },
]);
const result = await mock.client.escrows.get(project.id);
It also exports milestoneFixture, evidenceFixture, webhookFixture, and signWebhookEvent.
Security checklist
- Keep API and integration credentials server-side.
- Never store wallet private keys in Relay configuration.
- Validate the network, wallet addresses, USDC mint, milestone totals, and resolver before signing.
- Use full commit SHAs, frozen design versions, and checksums for immutable evidence.
- Use unique idempotency keys for create and submission workflows.
- Redact authorization headers and sensitive bodies from application logs.
- Review mainnet transactions before signing.
Examples
Runnable integrations are available in examples:
- Node.js project creation, funding, submission, release, and dispute flows
- Next.js route handler integration that keeps the API key server-side
- Express server integration with typed error handling
Development
npm ci
npm run typecheck
npm run lint
npm test
npm run build
Other scripts include test:coverage, format, and format:check.
Documentation and support
Publishing
- Update the package version and
CHANGELOG.md. - Run
npm ciandnpm run prepublishOnly. - Inspect the package with
npm pack --dry-run. - Publish with
npm publish --access publicfrom an authorized npm account.
License
MIT © PBJ Protocol. See LICENSE.