Backend service to read/write the deployed PayrollEscrow + WorkAgreement contracts over RPC.
- Copy env template:
cp env.example .env- Fill in:
STARKNET_RPC_URL- (optional)
PAYROLL_ESCROW_ADDRESS,WORK_AGREEMENT_ADDRESS
- Install + run:
npm install
npm run devIf you use pnpm:
pnpm install
pnpm run devFor production:
pnpm startGET /healthGET /api/v1/network/chain_idGET /api/v1/account/:address/nonce
POST /api/v1/auth/challengePOST /api/v1/auth/verify
GET /api/v1/escrow/:address/get_employerGET /api/v1/escrow/:address/get_agreementGET /api/v1/escrow/:address/get_token
POST /api/v1/prepare/escrow/:address/initializePOST /api/v1/prepare/escrow/:address/set_agreementPOST /api/v1/prepare/escrow/:address/depositPOST /api/v1/prepare/escrow/:address/releasePOST /api/v1/prepare/escrow/:address/refund_remaining
GET /api/v1/agreement/:address/get_employerGET /api/v1/agreement/:address/get_contributorGET /api/v1/agreement/:address/get_tokenGET /api/v1/agreement/:address/get_escrowGET /api/v1/agreement/:address/get_total_amountGET /api/v1/agreement/:address/get_paid_amount
POST /api/v1/prepare/agreement/:address/initialize_time_basedPOST /api/v1/prepare/agreement/:address/initialize_milestone_basedPOST /api/v1/prepare/agreement/:address/add_milestonePOST /api/v1/prepare/agreement/:address/approve_milestonePOST /api/v1/prepare/agreement/:address/claim_milestonePOST /api/v1/prepare/agreement/:address/activatePOST /api/v1/prepare/agreement/:address/pausePOST /api/v1/prepare/agreement/:address/resumePOST /api/v1/prepare/agreement/:address/cancelPOST /api/v1/prepare/agreement/:address/claim_time_based
By default the backend loads ABI from:
../Starknet-Contracts/target/release/starknet_contracts_PayrollEscrow.contract_class.json../Starknet-Contracts/target/release/starknet_contracts_WorkAgreement.contract_class.json
- The backend does not hold private keys.
- Users first prove wallet ownership by signing a backend-issued challenge (
/auth/challenge→ sign typed data →/auth/verify). - For contract mutations, the backend returns a prepared
call+nonce; the frontend wallet/account should sign + execute.
- Get challenge and sign it:
import { connect, type TypedData } from "starknet";
const BACKEND = "http://localhost:4000/api/v1";
async function login(address: string) {
const chRes = await fetch(`${BACKEND}/auth/challenge`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address }),
});
const ch = await chRes.json();
const typed: TypedData = ch.typed_data;
const conn = await connect();
if (!conn?.account) throw new Error("Wallet not connected");
const signature = await conn.account.signMessage(typed);
const vRes = await fetch(`${BACKEND}/auth/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address, signature }),
});
return await vRes.json(); // { session_token, ... }
}- Prepare a call (example: escrow deposit), then execute from wallet:
async function deposit({
walletAddress,
sessionToken,
escrowAddress,
amount,
}: {
walletAddress: string;
sessionToken: string;
escrowAddress: string;
amount: string; // decimal string
}) {
const prepRes = await fetch(`${BACKEND}/prepare/escrow/${escrowAddress}/deposit`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ wallet_address: walletAddress, session_token: sessionToken, amount }),
});
const prep = await prepRes.json(); // { call, nonce, chain_id, wallet_address }
const conn = await connect();
if (!conn?.account) throw new Error("Wallet not connected");
// Wallet signs + sends the transaction directly
return await conn.account.execute(prep.call, { nonce: prep.nonce });
}