Skip to main content
[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.appparmana-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.
The one thing to internalize before anything else: parmana-api-real and parmana-paytm-agent are two separate Vercel projects with two separate, independently-managed sets of environment variables. Nothing is shared between them automatically. Every piece of configuration below must be set on both, separately, even when the value happens to be identical (e.g. PAYTM_CONNECTOR_SHARED_SECRET) or even when the underlying resource is the same (e.g. DATABASE_URL — same Supabase project, but each service holds its own copy of the connection string).

Prerequisites

  • Write access to both Vercel projects (parmana-api-real and parmana-paytm-agent in 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 .env configured (see .env.example) — used to generate API keys and, optionally, to query the audit trail afterward.
  • PowerShell (Windows) or curl/bash (any OS).
  • npm install at the repo root, to run scripts/generate-api-key.ts.
Migrations required on the Supabase project first (idempotent, safe to re-run; this repo’s 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.sql
  • supabase/migrations/20260914130000_add_business_transaction_correlation_to_execution_audit_events.sql
  • supabase/migrations/20260914140000_add_authorization_verified_to_execution_audit_events.sql

The Supabase connection string trap

This single issue cost more real time than every other issue in this whole flow combined. Read this fully before setting DATABASE_URL anywhere.
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
The Direct hostname is IPv6-only for most Supabase projects; Vercel’s serverless functions only have IPv4 egress. The result isn’t a slow connection — it’s an immediate DNS failure: 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:
(Postgres error 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 /readyauthDisabled: false means yes, you need a real key):
Scope --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:
Note the returned 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:
Set the identical value on both projects:
  • On the Parmana deployment: PAYTM_CONNECTOR_URL=https://<your-connector-deployment> and PAYTM_CONNECTOR_SHARED_SECRET=<the value>
  • On the connector deployment: PAYTM_CONNECTOR_SHARED_SECRET=<the exact same value>
Missing either variable on the Parmana side means 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

The metadata block is easy to forget — the plain example on Connect an agent omits it, which is itself a real documentation gap found while building this guide. Don’t copy that example verbatim without adding it.
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 == true AND managerApproved == true AND fraudCheckPassed == true AND refundAmount <= 10000.
  • boundSignals binds signals.refundAmount to intent.parameters.amount — they must match, or SignalIntentBinder rejects it as a binding-tamper attempt before policy evaluation runs at all. Always set both to the same value.
The full, verified-working shape:
Use "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

The 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 successful 200 returns the complete ExecutionTrustRecord. Check, in order of “how far did this get”:
  1. executions[0].decision.outcomeAPPROVED/REJECTED, with .reason.
  2. executions[0].evidence.attributes.connector.responseSummary.success — whether Paytm itself confirmed the refund (false with synthetic IDs is expected, not a failure of the flow itself).
  3. executions[0].chainHash/.chainSignature — the execution was signed and chained.
  4. verifications[0].statusVERIFIED, proof the record was independently re-verified.
  5. receipts[0] — a separately signed receipt.
  6. authorization.payload — the full signed authorization (policyContentHash, signalsHash, businessTransactionHash, nonce, authorizationId).

Verifying the cross-service audit trail

Both deployments write to the same execution_audit_events table, correlated by businessTransactionId (not authorizationId — Parmana’s own authorization identity never crosses the wire boundary to the connector service):
A complete, correct run produces four rows: 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

Never paste a DATABASE_URL value containing a real password into a chat conversation, issue tracker, or any shared document. If one is ever exposed this way, rotate the database password immediately (Supabase Dashboard → Project Settings → Database → Reset database password), then update every deployment and local file that held the old value — see the password-rotation cascade above.
  • The raw caller API key is shown exactly once and never recoverable from its stored hash.
  • PAYTM_CONNECTOR_SHARED_SECRET is transport authentication only — never treat its presence as a substitute for the real Ed25519 authorization signature check.
  • Scope every generated key’s allowedCapabilities to 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).