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)
| Program | ID | Last deployed slot |
|---|---|---|
trustrail_pipelines | CgXidrts...ucoC8 | 508283076 |
mock_swap (disclosed stand-in) | 3qqN3ZXc...9vJ | 503154063 |
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
| Step | Tx |
|---|---|
| createStagingAccount | 4EX3mHHC...EKK3hT |
| create_commitment | 4PzKMc8c...o7ivHPs |
| withdraw_for_swap | 3WJGHca2...7cTcUKAF |
| mock_swap_execute | 3zFdaa8P...1HqXBhJS |
| submit_proof | 2DP3LxG8...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
| Step | Tx |
|---|---|
| createStagingAccount | Se7KyABQ...XmXinTC |
| create_commitment | 3FEYZcuH...EGWsTMHh3x9 |
| withdraw_for_swap | 3RvEgZnh...MEN34XSs |
| mock_swap_execute | dDGNfRD2...FWne |
| submit_proof | 3zHeaUES...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.
| Step | Tx |
|---|---|
program upgrade (adds recover) | 2BuVAHtP...3Vg6fqM |
| recover | 2LZ3VqM9...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)
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.
| Step | Tx |
|---|---|
| create_commitment | 2dJPE5Yx...8rrXMPM |
| withdraw_for_swap | 5FX7Zx2D...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)
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.
| Step | Tx |
|---|---|
| create_commitment | 5P4tbsW1...GVp8vyY |
| revoke delegate | 33wR5cPP...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)
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.
| Step | Tx |
|---|---|
| create_commitment | LFLceLhx...cuNK1tY |
| withdraw_for_swap | 22YP1vqJ...miKEPRr |
| mock_swap_execute | 3NmMnH1f...jucbQbU |
| submit_proof | 66LyJSn2...m2qvLVM |
Repo state: 457165a — same build note as Run C.
Slice 3 — release with an escrow sweep (Oct 7)
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.
| Step | Tx |
|---|---|
| program upgrade (adds the sweep) | 43bV1pbL...A9Q8EJC |
donate 1 input token to 2UmAP7rZ... escrow | 2LbX5LRu...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)
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.
| Step | Tx |
|---|---|
| create_commitment | 67N3fxP7...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
Current main:
49b63ad · Deployed program code: 7ede6d0 · github.com/Portaudit/trustrail-pipelines