Buyer and provider sign a single Solana transaction that writes the job spec to a PDA. Once that PDA exists, a validator can read the canonical terms straight from chain when they rule on a dispute.
Anchor the agreement before any USDC moves into escrow. If something goes wrong later, the dispute panel reads this PDA to know what the parties actually agreed to.
npm i @telaro/sacp @solana/web3.js
# or pnpm / bun, your call.The solo demo (buyer and provider are the same wallet) is one transaction. Multi-wallet uses a partial sign that gets shared with the other side.
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { agreement } from "@telaro/sacp";
import { sha256 } from "@noble/hashes/sha256";
const conn = new Connection("https://api.devnet.solana.com");
const buyer = wallet.publicKey; // from a wallet adapter
const provider = new PublicKey("<provider-pubkey>");
// PDA seed: sha256(human-readable jobId).
const jobIdHash = sha256(new TextEncoder().encode("acme-translate-#42"));
const ix = agreement.buildPostAgreementIx({
buyer,
provider,
jobId: jobIdHash,
// Pass 32 zero bytes if you don't want to commit a sample.
sampleRequestHash: sha256(new TextEncoder().encode("Translate to ko")),
sampleDeliverableHash: sha256(new TextEncoder().encode("최종본")),
specUri: "ipfs://Qm.../spec.md",
bondAtoms: 1_000_000n, // 1 USDC
});
const tx = new Transaction().add(ix);
tx.feePayer = buyer;
tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;
// Solo demo: one signTransaction covers buyer = provider.
// Multi-wallet: wallet.signTransaction (buyer slot only) +
// tx.serialize({ requireAllSignatures: false }), then share the
// bytes with the provider for a co-sign.
const signed = await wallet.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize());
await conn.confirmTransaction(sig);
const [pda] = agreement.deriveAgreementPda(jobIdHash);
console.log("agreement anchored:", pda.toBase58());Any validator UI can read the PDA from chain directly. The gateway does this too, then attaches the result to GET /dispute/:jobId.
import { agreement } from "@telaro/sacp";
const fetched = await agreement.fetchAgreement(conn, jobIdHash);
if (!fetched) {
// not anchored yet
} else {
console.log("buyer", fetched.buyer.toBase58());
console.log("provider", fetched.provider.toBase58());
console.log("bond", fetched.bondAtoms.toString(), "atoms");
console.log("spec", fetched.specUri);
console.log("signed at", fetched.signedAt.toString());
}| Field | Type | Notes |
|---|---|---|
| jobId | [u8; 32] | sha256 of the human-readable jobId. Same bytes the gateway uses for routing. |
| sampleRequestHash | [u8; 32] | sha256 of the sample input, or 32 zero bytes if you skip it. |
| sampleDeliverableHash | [u8; 32] | Same idea, but for the expected output. |
| specUri | string ≤ 256B | Public URL or IPFS hash. Validators read this for the full spec. |
| bondAtoms | u64 | Bond the provider commits. USDC uses 6 decimals (1 USDC = 1_000_000 atoms). |
2tp9TSMeuDyK117VvW3t2hJwfLLBMgZX8h4NCUsBLtvU. Mainnet pubkey lands when the audit clears.See also: Jury (VRF) for the panel draw that reads this agreement, and Quickstart for the wallet boilerplate.