Pay.sh adapter
Walk the live Pay.sh + AgentTrust integration end to end — challenge, retry, settle, feedback, with hosted-demo paths and the SERVICE-signed envelope contract.
Pay.sh is the Solana Foundation's first x402 facilitator, launched 2026-05-05 with Google Cloud. AgentTrust ships day-one Pay.sh integration as the canonical FacilitatorAdapter implementation. Source: trustgate/server/src/facilitators/pay-sh/.
What the adapter does
| Stage | x402 / Pay.sh wire shape | AgentTrust action |
|---|---|---|
| Initial request | no payment proof | emit 402 Payment Required with a base64 x402 v2 envelope |
| Challenge | paymentRequirements.extra.agentTrust | include payer agent, payee agent, payee recipient, policy ID, issuedAt, serviceSignature |
| Retry | PAYMENT-SIGNATURE or X-PAYMENT header | parse proof, rebuild VerifyContext |
| Policy | VerifyContext | run gate_payment decision |
| Allow | signed payment proof | validate transfer fields, emit feedback, forward resource |
| Deny | gate decision | return 402 with reason code + reason name |
Hit the live demo
demo.agenttrust.tech runs the full Pay.sh + AgentTrust pipeline against deployed devnet programs. With no payment proof:
curl -i https://demo.agenttrust.tech/protectedReturns HTTP/2 402 with a SERVICE-signed payment-required envelope. With Pay.sh installed, let the CLI pay and retry:
pay --sandbox curl https://demo.agenttrust.tech/protectedThe demo seeds three deterministic counterparties. Drive each branch by passing the matching X-Demo-Payer-Agent header:
# Allow path (tier 3)
PAYER=$(curl -s https://demo.agenttrust.tech/health | jq -r '.counterparties[2].agent')
pay --sandbox curl -H "X-Demo-Payer-Agent: $PAYER" https://demo.agenttrust.tech/protected
# Deny path (tier 0)
PAYER=$(curl -s https://demo.agenttrust.tech/health | jq -r '.counterparties[0].agent')
pay --sandbox curl -H "X-Demo-Payer-Agent: $PAYER" https://demo.agenttrust.tech/protected| Counterparty tier | Decision |
|---|---|
| 0 | 402 Deny — CounterpartyTierBelowMin |
| 1 | 402 Deny — CounterpartyTierBelowMin |
| 3 | 200 Allow + X-Payment-Receipt |
SERVICE-signed challenge
The SERVICE emits a signed challenge when it returns the Pay.sh 402 response. The signature binds:
issuedAt- network
- amount
- asset
payTo- payer agent
- payee agent
- payee recipient
- policy ID
- payment ID hash
PaySh.parseRequest() verifies that signature against the facilitator public key before accepting the request. This closes the race window where a forged paymentRequirements envelope could race a legitimate one.
Files to read in repo
| File | Purpose |
|---|---|
facilitators/pay-sh/index.ts | The five-method PaySh class |
facilitators/pay-sh/schemas.ts | Strict Zod wire schemas |
facilitators/pay-sh/sig.ts | SERVICE challenge signature helpers |
facilitators/pay-sh/proof-validator.ts | Replay, self-pay, amount, mint, recipient checks |
facilitators/pay-sh/feedback.ts | Idempotent feedback emission |
examples/pay-sh-demo/src/middleware.ts | Express bridge from Pay.sh retry to AgentTrust |
Production wiring
The demo proves the pipeline without requiring devnet RPC in CI. The server package carries the real devnet path: trustgate/server/src/chain.ts loads deployed program IDLs, /verify simulates gate_payment, and /settle delegates proof validation plus feedback emission through the active adapter.
Production swaps three dependency seams the demo stubs:
import { PaySh } from "./facilitators/pay-sh";
const adapter = new PaySh({
signingNetwork: "solana-devnet",
feePayer: facilitator.publicKey,
validateOnChainTx, // makeValidateOnChainTx({ connection, … })
emitFeedbackCpi, // makeEmitFeedbackCpi({ connection, programIds, … })
priorEmissionLookup, // makePriorEmissionLookup({ connection, programId })
replayCache, // ReplayCache (in-memory by default)
signDecision, // signs canonical decision bytes with facilitator key
});| Dependency | Production implementation |
|---|---|
validateOnChainTx | parse the confirmed SPL transfer transaction from RPC |
emitFeedbackCpi | build and send trustgate::emit_feedback through Anchor |
priorEmissionLookup | read the FeedbackEmissionLog PDA by payment hash |
signDecision | sign canonical decision bytes with the facilitator key |
The factories (makeValidateOnChainTx, makeEmitFeedbackCpi, makePriorEmissionLookup) are SDK exports — see SDK → Exports reference.
Failure cases the adapter must keep
Do not remove these checks when adapting Pay.sh into a production server:
- reject unknown schema fields at the root
- reject zero or u64-overflow amounts
- reject network mismatch
- reject
payTo/payeeRecipientmismatch - reject expired challenges
- reject replayed proof bindings
- reject transfer authority equal to facilitator fee payer (self-pay defense)
These are part of the adapter, not the route layer. The Pay.sh adapter's proof-validator.ts is the reference implementation.
Live evidence
Real atomic-settlement trace on devnet (2026-05-06):
| Step | Tx |
|---|---|
Signed SPL transferChecked | 5iV8EYmJh9XS… |
emit_feedback PDA-signed CPI | jMobmWJUAXuL8… |
FeedbackEmissionLog PDA | HB4BBi9j… |
Full trace + reproduction commands: Verification → Devnet smoke.