TrustGate
The facilitator-side Anchor program — PDA-signed feedback CPIs into Quantu and per-payment idempotency receipts.
TrustGate is the program a facilitator owns. One PDA per facilitator (TrustGateAuthority) signs CPIs into Quantu's agent-registry-8004::give_feedback. One PDA per payment (FeedbackEmissionLog) is the on-chain idempotency receipt. Three instructions.
Devnet: HF8zHfoyA7b5mhLViopTnRMprc6ZT5KActHTdkFrih2N
Instructions
| Instruction | Effect |
|---|---|
init_authority(facilitator) | Create TrustGateAuthority PDA for facilitator. One-time per facilitator. |
emit_feedback(payment_id_hash, facilitator, payee_asset, score, tag1, tag2, endpoint, feedback_uri) | PDA-signed CPI into agent-registry-8004::give_feedback. Initialises FeedbackEmissionLog. |
dispute_payment(payment_id_hash, facilitator, payee_asset, dispute_reason_hash, feedback_uri) | PDA-signed CPI emitting NEGATIVE-score feedback (score = DISPUTE_SCORE, tag1 = dispute). Initialises FeedbackEmissionLog. |
Both emit_feedback and dispute_payment enforce signer == facilitator (FacilitatorSignerMismatch). Without that guard, anyone could write fake feedback under any facilitator's authority — a real attack against on-chain reputation.
State accounts
TrustGateAuthority
#[account]
pub struct TrustGateAuthority {
pub facilitator: Pubkey, // off 8..40 — external facilitator wallet
pub bump: u8, // off 40
pub _pad0: [u8; 7], // off 41..48
pub feedback_count: u64, // off 48..56 — total successful give_feedback CPIs
pub dispute_count: u64, // off 56..64 — total dispute_payment CPIs
pub created_at_slot: u64, // off 64..72
pub _reserved: [u8; 32], // off 72..104
}PDA seeds: ["trustgate_auth", facilitator]. Different facilitators (Pay.sh, Dexter, atxp, MCPay) each get their own PDA, so feedback emission is namespaced per-facilitator on-chain. The feedback_count and dispute_count accumulate as that facilitator emits.
FeedbackEmissionLog
#[account]
pub struct FeedbackEmissionLog {
pub payment_id_hash: [u8; 32], // off 8..40 — caller-supplied payment ID hash
pub bump: u8, // off 40
pub score: u8, // off 41 — score that was emitted
pub is_dispute: u8, // off 42 — 1 = dispute path, 0 = emit_feedback path
pub _pad0: [u8; 5], // off 43..48
pub emitted_at_slot: u64, // off 48..56
pub _reserved: [u8; 16], // off 56..72
}PDA seeds: ["feedback_log", payment_id_hash]. The init-only constraint is the idempotency mechanism: a second emit_feedback (or dispute_payment) for the same payment_id_hash fails with account-already-in-use and the entire transaction reverts. There is no recovery path — that is exactly the desired property: a successful CPI mints exactly one feedback per payment.
The caller computes payment_id_hash = SHA-256(payment_id_string) off-chain. The SDK does this in composeAtomicSettleTx; facilitators that build their own settlement path replicate the same hash construction.
PDA-signed CPI to Quantu
emit_feedback PDA-signs the CPI into Quantu's agent-registry-8004::give_feedback:
let signer_seeds: &[&[u8]] = &[
TrustGateAuthority::SEED_PREFIX, // b"trustgate_auth"
facilitator.as_ref(),
&[bump],
];
invoke_give_feedback(&cpi_accounts, &args, signer_seeds)?;Quantu's give_feedback instruction expects the caller to be the agent's owner — TrustGate's PDA is registered as the feedback authority via Quantu's identity flow on first use. The client account in the CPI is set to authority.to_account_info(), so Quantu sees a PDA-signed call rather than the facilitator's wallet directly.
remaining_accounts ordering for the CPI:
| Index | Account | Notes |
|---|---|---|
| 0 | agent_account | payee's Quantu AgentAccount PDA |
| 1 | asset | payee's Metaplex Core asset |
| 2 | collection | Quantu's collection asset |
| 3 | system_program | for rent on AtomStats init |
| 4 | atom_config (optional) | Quantu's AtomConfig PDA |
| 5 | atom_stats (optional) | payee's AtomStats PDA — created on first feedback |
| 6 | atom_engine_program (optional) | Quantu's atom-engine program |
| 7 | registry_authority (optional) | Quantu's registry authority PDA |
Indexes 0..=3 are required (AgentRegistryProgramMismatch on count mismatch). Indexes 4..=7 come as a group of 4 — pass all four or none. The SDK's composeAtomicSettleTx builds this account list correctly; integrators going lower-level should consult programs/trustgate/src/ext/agent_registry.rs.
give_feedback args
pub struct GiveFeedbackArgs {
pub value: u64,
pub value_decimals: u8,
pub score: Option<u8>, // 0..=100
pub feedback_file_hash: Option<[u8; 32]>, // dispute_reason_hash for disputes
pub tag1: String, // ≤ 32 bytes
pub tag2: String, // ≤ 32 bytes
pub endpoint: String, // ≤ 64 bytes
pub feedback_uri: String, // ≤ 256 bytes
}emit_feedback validates score ≤ 100, tag1.len() ≤ 32, tag2.len() ≤ 32, endpoint.len() ≤ 64, feedback_uri.len() ≤ 256. Discriminator: [145, 136, 123, 3, 215, 165, 98, 41] (Reference → Discriminator constants).
Disputes
dispute_payment is the negative-feedback path. Per docs/plan/research/04-policyvault-build-playbook.md §C.4, revocations are events-only; new negative feedback is the canonical "downstream consumers see and react to" signal. The handler:
- Requires
dispute_reason_hash != [0; 32](DisputeReasonRequired). - Hard-codes
score = DISPUTE_SCORE,tag1 = "dispute". - Sets
tag2to the first 16 hex chars of the dispute reason hash for off-chain consumers. - Sets
is_dispute = 1on theFeedbackEmissionLog.
Same idempotency pattern: the init constraint blocks double-disputes per payment_id_hash.
Events
| Event | Fields |
|---|---|
AuthorityInitialized | facilitator, authority, slot |
FeedbackEmitted | facilitator, payee_asset, payment_id_hash, score, slot |
PaymentDisputed | facilitator, payee_asset, payment_id_hash, dispute_reason_hash, slot |
Live trace
Real emit_feedback CPI on devnet, captured 2026-05-06: jMobmWJUAXuL8FmQujfxW9NmeMbzADUoABzqjiMeuc5m3YXyeuZeUw1ZJc29JGsqyWQGDY8q3vrtBdamhKXraag — the resulting FeedbackEmissionLog PDA at HB4BBi9jaD3VPcZkQQaH3DxukSqBiXfW8RejtaLa8bF3 has score=100, is_dispute=0. Full trace: Verification → Devnet smoke.
Source
- Program entry:
programs/trustgate/src/lib.rs emit_feedback:instructions/emit_feedback.rsdispute_payment:instructions/dispute_payment.rsinit_authority:instructions/init_authority.rs- CPI wrapper:
ext/agent_registry.rs - State:
state/