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.app → parmana-paytm-agent.vercel.app → Paytm’s staging API) on 2026-09-14. Nothing here is theoretical.
Since then every refund needs a signed manager approval. The 2026-09-14 run used customer-refund 1.0.0, where a small refund ran on the agent’s claims alone. customer-refund 1.2.0 has been in effect since 2026-09-28: the request below now carries managerApproved: true and the manager’s signed approval in signals.approvalArtifact (see Human approval). The full flow with an approval has not been rerun live against Paytm staging since; the wiring, errors and fixes below are unchanged.

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).
Calling through the agent’s own /agent/refunds (added 2026-09-28). An agent can also call parmana-paytm-agent’s POST /agent/refunds, which calls POST /execute above on its behalf. The flow is the same: Parmana alone sends the refund to Paytm, once, through /connector/paytm-refund, and /agent/refunds returns Paytm’s result from the Trust Record. Until 2026-09-28 that endpoint also called Paytm itself after an approval, so an approved refund could be paid twice (G-70, fixed in that repository’s PR #6). It now also reads the policy version from GET /policies/in-effect before every refund.

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). Apply every pending migration with npm run db:migrate -- apply and check with npm run db:migrate -- status; these three are the ones this flow added:
  • 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 /ready → authDisabled: 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 (connectors are optional by default, deliberately): an approved refund is then refused with 503 CONNECTOR_NOT_REGISTERED and nothing is sent. 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. Without it the request is refused with 400 before policy runs. (The example on Connect an agent has included it since this guide found the gap.)
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.2.0, exact values):
  • Every refund above 0 and up to 100000 is approved only with refundEligible and fraudCheckPassed true, managerApproved: true and a signed manager approval in signals.approvalArtifact. Parmana verifies the signature, the order and the amount; managerApproved: true without a valid approval is refused. See Human approval.
  • 0 or less, or above 100000, refused.
  • Production runs the version most recently approved; ask GET /policies/in-effect?capability=paytm:refund. 1.2.0 has been in effect in production since 2026-09-28: there is no amount that runs without a signed manager approval. managerApproved: false is always refused.
  • 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 request shape, for customer-refund 1.2.0:
Use "authorityType": "SERVICE", "AGENT" doesn’t exist in AuthorityType. Optional parameters (added 2026-09-28, G-71). intent.parameters may also carry: Anything else in intent.parameters is refused before any call to the connector. Synthetic order/transaction IDs (not a real prior Paytm charge) are fine for a connectivity and wiring test: with a valid signed approval 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). Add -TimeoutSec 120: an approved request can take tens of seconds (G-84), and after a timeout read GET /trust-records/<businessTransactionId> before sending anything again. 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.outcome, APPROVED/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].status, VERIFIED, 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, with every response the server can return.