TrustRail emblem — a shield

TrustRail

Deterministic settlement for AI agent commerce on Solana

TrustRail settles outcomes. Not requests.

Scope disclosure

The swap step in every run below that includes one executes against mock_swap, a program I built and deployed myself as an explicit stand-in for a real swap venue (fixed 1:1 rate and pool liquidity seeded once by a trusted signer, per the source; real token transfers, real failure on insufficient pool liquidity — not a simulation). Run C, Run C2 and the refund run never reached a swap. It is not Orca, Jupiter, Raydium, or any production DEX. Passed attests only that the output account held at least the minimum, not that a swap happened, and the worker or the staging-key holder chooses where swap output lands; the program does not enforce it. Full detail: KNOWN_LIMITATIONS.md.

Programs (devnet)

ProgramIDLast deployed slot
trustrail_pipelinesCgXidrts...ucoC8508283076
mock_swap (disclosed stand-in)3qqN3ZXc...9vJ503154063

Try it yourself

Requires: Node 20+, a funded Solana devnet keypair (solana airdrop works), Anchor CLI if you want to rebuild from source.

git clone https://github.com/Portaudit/trustrail-pipelines
cd trustrail-pipelines/worker
npm install
npm run create-commitment
npm run withdraw-for-swap
npm run execute-mock-swap
npm run submit-proof
npm run get-commitment -- <commitment-address>

These are the exact scripts in worker/package.json — nothing here is simplified from what's actually in the repo.

Recovery and cancel are not npm scripts. From worker/, with a separate keypair file as settler (it must not be the commitment's payer; the scripts refuse that, it is a script choice and not a program rule): SETTLER_KEYPAIR=<path> npx tsx src/recover.ts <commitment-address> or SETTLER_KEYPAIR=<path> npx tsx src/cancel.ts <commitment-address>. recover applies to a FailedSlippage commitment whose escrow was withdrawn, or to a Locked one with the escrow withdrawn and no proof stamped, once the deadline slot has passed. cancel applies to a Locked commitment whose escrow was never withdrawn, after the deadline. release is run the same way: SETTLER_KEYPAIR=<path> npx tsx src/release.ts <commitment-address>. It applies to a Passed commitment, and unlike the other two it does not refuse a settler equal to the payer, because the program allows it. refund is run the same way with src/refund.ts; it applies to a FailedSlippage commitment whose escrow was never withdrawn.

Run B used an input of 300000 and a floor of 500000. When it ran, the scripts hardcoded an input of 1,000,000. They now read INPUT_AMOUNT, MIN_OUTPUT_AMOUNT and DEADLINE_SLOTS from the environment (defaults 1,000,000, 500,000 and 5000 slots), so Run B's input can be reproduced, and Run C used INPUT_AMOUNT=250000 DEADLINE_SLOTS=900.

Optional: set RPC_URL to your own devnet RPC endpoint (the public devnet endpoint can fail under load). If unset, the scripts fall back to the public devnet.

Verified runs

Runs A, B and D follow the flow up to the recorded verdict — commitment → escrow withdrawal → swap → deterministic verification (Passed or FailedSlippage) — independently decoded from raw on-chain account bytes, not inferred from script output. Runs A and B list five transactions each: creating the staging account is its own step, separate from the swap execution. Run D lists four. Run C, Run C2 and the refund run never reached a swap and are described in their own sections.

Run A — Passed

task_id: a7c709942f0542765cea54444823f7d4a5606d62575b4a71de02209a08deb96c
commitment: DeoSo4Dy...GUB3Ur
final state: status: Passed, escrowWithdrawn: true
StepTx
createStagingAccount4EX3mHHC...EKK3hT
create_commitment4PzKMc8c...o7ivHPs
withdraw_for_swap3WJGHca2...7cTcUKAF
mock_swap_execute3zFdaa8P...1HqXBhJS
submit_proof2DP3LxG8...JG1RF

Repo state: 95cd061 — this code was deployed to devnet before that commit was pushed (a known gap in my workflow that night: build → deploy → verify happened first, git catch-up came after). The commit timestamp trails the on-chain deploy; it does not indicate the page claiming ahead of the repo.

Run B — FailedSlippage

task_id: b66b8e4b7f479828d25d8b2c4cf1e8f9352c1953b978f03fca7433845ca82ad9
commitment: HAi7f8Qf...HMyiPA
final state: status: FailedSlippage, escrowWithdrawn: true
StepTx
createStagingAccountSe7KyABQ...XmXinTC
create_commitment3FEYZcuH...EGWsTMHh3x9
withdraw_for_swap3RvEgZnh...MEN34XSs
mock_swap_executedDGNfRD2...FWne
submit_proof3zHeaUES...Zkj13b6yo

Repo state: 95cd061 — same caveat as Run A above.

Update, Oct 5, 2026: recovered. I recovered this commitment after the receipts above were recorded. The five receipts and the final-state line above were true when those transactions ran and are unchanged; the commitment's status is now Recovered. Before recovery: escrow 0, output account 300000, staging 0, payer output account absent. After: escrow and output accounts closed, staging 0, payer output account 300000, payer input account unchanged at 700000. A settler that is not the payer signed it. Only the swap output came back. This is recovery of stuck proceeds, not a refund: the 300000 input went into the mock pool during the run.

StepTx
program upgrade (adds recover)2BuVAHtP...3Vg6fqM
recover2LZ3VqM9...hpr4R2C

Repo state: 80c3d18 — the binary behind these two transactions was built locally with anchor build from those sources, and no tracked file was modified after the build (by file timestamps). It is not a verifiable or reproducible build.

Run C — recover on a Locked commitment after the deadline (Oct 5)

commitment: 4ywZV4cW...9tAFFu
input 250000, minimum output 500000, deadline 900 slots (create landed in slot 507593103, deadline 507593999)

I moved the staging key out of the worker folder first. withdraw_for_swap moved 250000 from escrow to staging. An early recover at slot 507593356 failed with 0x1775 (DeadlineNotReached) and changed nothing. After the deadline a settler that is not the payer recovered it in slot 507594044: payer input 700000 to 950000, escrow and output accounts closed, staging 0, status Recovered, and no worker key involved. A second recover failed with AccountNotInitialized (3012, 0xbc4) and the status stayed Recovered. Reading the staging account afterwards, its delegate allowance was 18446744073709301615: approved for u64::MAX, reduced by the 250000 that recover pulled.

StepTx
create_commitment2dJPE5Yx...8rrXMPM
withdraw_for_swap5FX7Zx2D...6t6pQaE
recover (after deadline)5ZuvUcFt...UNUoUVi

Repo state: 457165a — the deployed binary was built locally from those sources (file timestamps and a dump-hash check after the upgrade); it is not a reproducible build.

Run C2 — staging guard, then cancel (Oct 5)

commitment: ByYUeb7F...it2eXV
input 200000, minimum output 500000, deadline 1500 slots

The staging key revoked the delegate. withdraw_for_swap then failed with 0x177b (StagingNotPullable) in simulation, so nothing was sent and escrow still held 200000. After the deadline, cancel by a settler that is not the payer (slot 507596533): escrow 200000 to closed, payer input 950000 to 1150000, status TimedOut.

StepTx
create_commitment5P4tbsW1...GVp8vyY
revoke delegate33wR5cPP...r7AeYnf
cancel (after deadline)CaRXK5SU...Zux9LfG

Repo state: 457165a — same build note as Run C.

Run D — normal path with the delegate in place (Oct 5)

commitment: 5zrxT6Ad...yPhre2
final state: status: Passed

This run reached Passed. I left it unreleased on purpose at the time, and released it on Oct 7 (see Slice 3 below). The proofTx field in the decoded account is random bytes, not a transaction, so none of the receipts cite it.

StepTx
create_commitmentLFLceLhx...cuNK1tY
withdraw_for_swap22YP1vqJ...miKEPRr
mock_swap_execute3NmMnH1f...jucbQbU
submit_proof66LyJSn2...m2qvLVM

Repo state: 457165a — same build note as Run C.

Slice 3 — release with an escrow sweep (Oct 7)

Upgrade to the Slice 3 program: last deployed slot 508283076. Binary 268368 bytes, sha256 a50c98d07dc0bdaf7aa560d3ff4f2b5246bb2e447f81069e4f61bda96f40a798.

release now sweeps any input token left in escrow to the payer before closing it, which removes the lock-up where one donated token made release fail at the escrow close. Before the upgrade I donated one input token to the escrow of an older Passed commitment (2UmAP7rZ...). With the deployed Slice 2 program, a simulated release failed at the escrow close with token error 0xb, and nothing was sent. After the upgrade the same commitment released and the payer received the donated token. Run D, which had no dust, was then released too. Both releases are Finalized. The swap in these runs is the disclosed mock_swap stand-in, not a real venue.

StepTx
program upgrade (adds the sweep)43bV1pbL...A9Q8EJC
donate 1 input token to 2UmAP7rZ... escrow2LbX5LRu...p6pbFG3
release 2UmAP7rZ... (slot 508283971)NQ16U67m...DP6mFK6
release Run D (slot 508284268)2kU4tMx6...7a4WrQR

Before: escrow 1, payer input 1149999, payer output 300000. After: escrow and output accounts closed, status Released, payer input 1150000 (the donated unit), payer output 1300000. Run D: escrow 0, payer input unchanged at 1150000, payer output 1300000 to 2300000, status Released.

Repo state: 7ede6d0 — the deployed binary was built locally from that commit, with a clean tree. A dump of the buffer matched the hash before the upgrade, and a dump of the deployed program matched it after, with no non-zero padding. It is not a reproducible build. The output-donation variant of the lock-up is covered in litesvm only, not on devnet.

Refund run — a commitment that never reached the swap (Oct 7)

commitment: FWaLntwj...Wq5DU
final state: status: Refunded

This is not the normal flow. I created a commitment, did not call withdraw_for_swap, and submitted a proof claiming 0 against an empty output account, so the status became FailedSlippage with the escrow never withdrawn. refund then returned the escrow to the payer. Signed by the main wallet and Finalized. The normal worker flow withdraws first, so its failed runs exit through recover, not refund.

StepTx
create_commitment67N3fxP7...UXobrcE
submit_proof (claim 0)5KaYbG1S...YkJcaYp
refund (slot 508392421)3JdMXHRs...4Xy9iJc

Before: escrow 1000000, output 0, payer input 1150000, payer output 2300000. After: escrow and output accounts closed, status Refunded, payer input 2150000, payer output unchanged at 2300000.

Repo state: ea0e445 adds refund.ts; the program binary is the Slice 3 build (7ede6d0), unchanged. The second-refund error (3012) was observed in litesvm only.

Correction, Oct 5, 2026: earlier versions of this page described the flow as ending in release/refund, called the page's devnet runs "verified end-to-end", and said "refund on FAIL". That overstated it. Run A and Run D end at Passed; Run B reached FailedSlippage and was then recovered; Run C was recovered after its deadline; Run C2 was cancelled after its deadline. cancel is now exercised: in the litesvm tests and in devnet Run C2. release and refund exist in the program, but I have not exercised them in any documented devnet run. refund is reachable only for a FailedSlippage commitment whose escrow was never withdrawn, and the normal worker flow withdraws first, so those runs exit through recover. The page description, the social description and the flow sentence have been changed to match. None of this shows a complete settlement, release included, working end to end.

Amendment, Oct 7, 2026: the correction above is kept as written, but its statement that release has not been exercised in any documented devnet run is superseded by this amendment. Since then release has run on devnet twice, in the Slice 3 section above, both Finalized and both against the disclosed mock_swap stand-in. refund has since run once on devnet, on a commitment that never reached the swap (Refund run above); it has not run in the normal flow. Fifteen older commitments remained Passed and unreleased in a listing at slot 508312883, taken before the refund run. The fee is a design only and is not implemented. This is a documented devnet flow of lock, withdraw, mock swap, proof and release, not a claim about any production swap venue.

This section is appended to, not rewritten. Once a real swap-venue integration is green, a run against it joins here and the scope disclosure above is amended — never softened, only added to.

Lineage

TrustRail's no-judge design descends from AuditAgent — a prior Colosseum submission, not selected — whose semantic-gate verdict taught me that settlement must be spec-matching, never judgment. Frozen history, public corrections: theauditagent.xyz

The page never claims more than the repo's latest commit; every embedded receipt has a commit that produced it.
Current main: 49b63ad · Deployed program code: 7ede6d0 · github.com/Portaudit/trustrail-pipelines
Sentinel artwork — © Ishvir and Company (Pty) Ltd