[VERIFIED LIVE]. Every command, error message, and fix below is verbatim
from a real session that ran this exact flow against real, live, production
infrastructure (
parmana-api-real.vercel.app →
parmana-paytm-agent.vercel.app → Paytm’s staging API) on 2026-09-14. Nothing
here is theoretical. Repo copy of this guide, kept in sync:
END-TO-END-FLOW.md (repo root) — that file is the canonical, plain-markdown
source; this page is the same content organized for the site.Who this is for
Anyone who needs to run this flow again from zero — a new environment, a new operator, or future-you after forgetting the details. No step below assumes you remember anything from the session that produced it. Where something failed before it worked, the failure is documented too (see the error catalog), because the failure and its fix are exactly what a fresh attempt is most likely to repeat.Architecture: what actually happens, hop by hop
Three separate systems, three separate deployments, three separate configurations. Confusing any two of them costs real time — most of the error catalog below is exactly that.Prerequisites
- Write access to both Vercel projects (
parmana-api-realandparmana-paytm-agentin the real run this guide is based on — yours will be named differently). - Write access to the Supabase project both deployments share.
- A local clone of this repository with its own
.envconfigured (see.env.example) — used to generate API keys and, optionally, to query the audit trail afterward. - PowerShell (Windows) or
curl/bash (any OS). npm installat the repo root, to runscripts/generate-api-key.ts.
npm run migrate doesn’t actually work — see docs/VERIFICATION-GAPS.md G-36 — apply
these directly via a pg connection instead):
supabase/migrations/20260914120000_add_execution_audit_events.sqlsupabase/migrations/20260914130000_add_business_transaction_correlation_to_execution_audit_events.sqlsupabase/migrations/20260914140000_add_authorization_verified_to_execution_audit_events.sql
The Supabase connection string trap
Use the Transaction pooler connection string, always, for any Vercel/serverless deployment — never the Direct connection string. Supabase’s Connect dialog offers both:- Direct:
postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres - Transaction pooler (use this one):
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
getaddrinfo ENOTFOUND db.<ref>.supabase.co. It works perfectly from your own laptop and
fails 100% of the time from Vercel — that mismatch is the signature of this exact issue.
Notice the username also changes, from plain postgres to postgres.<project-ref> — the
pooler (Supavisor) needs that suffix to route the connection.
The [YOUR-PASSWORD] placeholder: Supabase’s Connect dialog shows the connection string
as a template with a literal [YOUR-PASSWORD] token in it — not your real password. You
must replace the entire token, brackets included, with your actual password (only ever
shown once, at project creation or at reset time — Project Settings → Database → Reset
database password). Leaving the placeholder in, or leaving the brackets around a
correctly-substituted password, produces:
28P01.) This error reports the bare role name postgres even when your
username format is correct (postgres.<project-ref>) — Supavisor normalizes that in its own
error output, so don’t chase it as a separate bug.
The password-rotation cascade: resetting the database password (for any reason,
including a credential accidentally exposed and rotated — see
Security) breaks every deployment holding the old password
simultaneously. In the real run this meant updating and redeploying parmana-api-real,
separately updating and redeploying parmana-paytm-agent, and separately updating this
repo’s own local .env — three distinct places, each producing the identical error message
if missed, so each must be checked individually rather than inferred from one working.
Propagation lag vs. a real problem: right after a genuine, correct redeploy, a request
can fail for a few seconds to about a minute while Vercel finishes routing to the new
deployment. Retry 2-3 times, a few seconds apart, before concluding a fix didn’t work. If
retries return the identical error every time with no change, it’s not lag — it’s real.
Getting the real error behind a generic 500: parmana-api-real’s error handler
deliberately returns {"error":"Internal Server Error"} for anything unrecognized (never
leak internals to a caller) — the real error, with a full stack trace, is only in Vercel
Dashboard → that project → Deployments → the relevant deployment → Runtime Logs. This is
how the real 28P01 password error was actually found in the session this guide is based on.
Generating a caller API key
Check whether caller-auth is enabled first (GET /ready → authDisabled: false means yes,
you need a real key):
--allowed-capabilities narrowly — never "*" for a single-purpose test. The raw key
is shown exactly once and cannot be recovered afterward; save it immediately somewhere
durable (a password manager — never a chat log, see Security).
Add the printed JSON entry to PARMANA_API_KEYS on the target deployment (Vercel dashboard
→ that project → Environment Variables). It’s a JSON array — append to an existing one,
or wrap a first entry in [ ]. Save, then trigger an actual new deployment (saving an env
var alone does not restart the running one).
Verify before attempting /execute:
allowedPrincipalIds — your test request’s authority.principalId must
be one of these values, or it’s rejected before policy ever runs.
Wiring the Paytm connector’s shared secret
GatewayPaytmAdapter authenticates itself to the remote connector service with a bearer
shared secret — transport authentication only, never a substitute for the real Ed25519
policy-approval signature. Generate one:
- On the Parmana deployment:
PAYTM_CONNECTOR_URL=https://<your-connector-deployment>andPAYTM_CONNECTOR_SHARED_SECRET=<the value> - On the connector deployment:
PAYTM_CONNECTOR_SHARED_SECRET=<the exact same value>
paytm:refund simply isn’t registered as
a capability at all (this codebase’s connectors are optional by default — see
docs/VERIFICATION-GAPS.md’s “Gaps checked and found not applicable” entry on that
design). A mismatch between the two returns {"error":"unauthorized"} (401) instantly.
Redeploy both after setting these.
Building the request body
Structural rules, checked before policy ever runs (BusinessTransactionValidator):
businessTransactionId is an idempotency key. Reusing one across attempts — including
while debugging with the same test payload — eventually returns 409:
"Business Transaction '<id>' already exists.", because the record persisted on a prior
attempt even if that attempt’s overall response was never seen. Generate a fresh UUID on
every real attempt.
Policy thresholds (customer-refund@1.0.0, exact current values):
- Approved iff
refundEligible == trueANDmanagerApproved == trueANDfraudCheckPassed == trueANDrefundAmount <= 10000. boundSignalsbindssignals.refundAmounttointent.parameters.amount— they must match, orSignalIntentBinderrejects it as a binding-tamper attempt before policy evaluation runs at all. Always set both to the same value.
"authorityType": "SERVICE" — "AGENT" doesn’t exist in AuthorityType.
Synthetic order/transaction IDs (not a real prior Paytm charge) are fine for a
connectivity/wiring test — the request is approved, correctly signed, correctly verified,
and correctly forwarded; Paytm’s staging API simply returns a non-success result because
there’s nothing real to refund. That proves the wiring, not that a real refund occurred —
don’t conflate the two.
Running the test
try/catch is not optional — Invoke-WebRequest throws on any non-2xx response
instead of returning it; without catching $_.ErrorDetails.Message you lose the actual JSON
error body, which is where every diagnostic in this guide came from. Run each statement
separately in an interactive session, and double-check your working directory before
re-running (Get-Content is relative to it).
Equivalent with curl:
Reading the response
A successful200 returns the complete ExecutionTrustRecord. Check, in order of “how far
did this get”:
executions[0].decision.outcome—APPROVED/REJECTED, with.reason.executions[0].evidence.attributes.connector.responseSummary.success— whether Paytm itself confirmed the refund (falsewith synthetic IDs is expected, not a failure of the flow itself).executions[0].chainHash/.chainSignature— the execution was signed and chained.verifications[0].status—VERIFIED, proof the record was independently re-verified.receipts[0]— a separately signed receipt.authorization.payload— the full signed authorization (policyContentHash,signalsHash,businessTransactionHash,nonce,authorizationId).
Verifying the cross-service audit trail
Both deployments write to the sameexecution_audit_events table, correlated by
businessTransactionId (not authorizationId — Parmana’s own authorization identity never
crosses the wire boundary to the connector service):
session.created and execution.completed
(Parmana’s own, signed and chained), and authorization.verified and execution.rejected
or execution.completed (the connector service’s own, unsigned — it never holds a private
key). The connector-side execution.rejected reflecting Paytm’s non-success outcome and
Parmana’s own execution.completed are not contradictory — the connector call itself
completed cleanly (no exception); Paytm’s own non-success business outcome lives in the
Execution Trust Record’s evidence, by design (see docs/connectors/PAYTM_CONNECTOR.md’s
“Paytm execution outcome” section).
Error catalog
Every one of these was actually hit, in this order, in the real run this guide is based on.A meta-lesson: at one point, a fix-list assumed a different symptom (empty
response body, silent crash) than what direct testing had just shown (a
specific, informative error message). Executing a prescriptive procedure
against a premise that doesn’t match the evidence in hand wastes time and can
add unneeded code changes. Before following any troubleshooting procedure —
including this one — check its stated symptoms against what you’re actually
observing.
Security notes
- The raw caller API key is shown exactly once and never recoverable from its stored hash.
PAYTM_CONNECTOR_SHARED_SECRETis transport authentication only — never treat its presence as a substitute for the real Ed25519 authorization signature check.- Scope every generated key’s
allowedCapabilitiesto exactly what’s needed — never"*"for a single-purpose test or agent.
Next
Live API and Demos
The full live-deployment reference this guide builds on — authentication,
every endpoint, every available policy.
Connect an external agent
The general agent-integration guide (note: its request example is missing
the
metadata block this guide’s error catalog covers).