@fidacy/sdk
v0.2.1
Published
Thin, typed client for the public Fidacy API. Bundles @fidacy/verify so you can check every signed verdict yourself.
Readme
@fidacy/sdk
A thin, typed client for the public Fidacy API. It ships @fidacy/verify alongside it, so the verdict you get back is one call away from being independently verified.
Fidacy returns a signed verdict for an agent payment. The point of the SDK is not to make you trust that verdict: it is to get it, and hand you the tool to check the signature yourself.
Install
npm i @fidacy/sdkAssess, then verify
import { Fidacy } from '@fidacy/sdk';
const fidacy = new Fidacy({ apiKey: process.env.FIDACY_API_KEY! }); // fky_test_… or fky_live_…
const result = await fidacy.assess({
mandate: {
vct: 'mandate.payment.1',
payee: { id: 'merchant_demo', name: 'Demo Store' },
payment_amount: { amount: 4299, currency: 'EUR' },
payment_instrument: { id: 'pi_demo', type: 'card' },
},
});
console.log(result.decision); // 'approve' | 'review' | 'deny'
console.log(result.score); // 0..100
// Don't trust the response object — verify the signed payload yourself.
const verified = await fidacy.verify(result.riskPayloadJws);
console.log('signature valid:', verified.valid);
console.log('decisions match:', verified.claims.decision === result.decision);fidacy.verify is @fidacy/verify's verifyRiskPayload, bundled so you don't add a second dependency. The assess call itself does no verification: it returns the API's response as-is, and you decide whether to trust it by checking the signature against Fidacy's public JWKS.
Not just payments
The verdict is action-generic. kind (default 'ap2_payment') accepts every action the engine assesses: 'ap2_payment' | 'message_send' | 'voice_call' | 'claim_document' | 'custom' (the exported AssessKind type). An agent sending a customer message, for example:
const verdict = await fidacy.assess({
kind: 'message_send',
mandate: {
kind: 'message_send',
actor_agent: 'did:web:agents.example.com:support-1',
principal: 'org:example',
channel: 'email', // 'whatsapp' | 'email' | 'sms'
recipient_ref: 'cust_8f31', // pseudonymized ref — never a raw address
content_hash: 'sha256-9b2f…', // hash only — the message never leaves you
requested_at: Math.floor(Date.now() / 1000), // epoch seconds
},
});
// verdict.riskPayloadJws is the signed, independently verifiable record
// that this send was assessed before it happened.Each kind validates against its public JSON schema on the engine side; the payment path (ap2_payment) is unchanged and remains the default.
What assess returns
interface AssessResult {
decision: 'approve' | 'review' | 'deny';
score: number; // 0..100
assessmentId: string;
riskPayloadJws: string; // the signed verdict — pass to fidacy.verify
signingKeyId: string;
signals: Record<string, unknown>;
outcome: Record<string, unknown>;
spend_guard?: Record<string, unknown>;
a2a?: { recommended_task_state: string; task_metadata: Record<string, unknown> };
}Webhooks
Webhook events are verified for you. constructEvent checks the signature before returning, and throws if it does not hold.
const event = await fidacy.webhooks.constructEvent(
rawBody,
req.headers['x-fidacy-signature'],
);
// event.type, event.data — only reached on a valid signature.Billing
const status = await fidacy.billing.get();
const { url } = await fidacy.billing.checkout({ tier: 'growth' });Errors
A non-2xx response throws FidacyError. The message is static and never includes your key or the request body.
import { FidacyError } from '@fidacy/sdk';
try {
await fidacy.assess({ mandate });
} catch (err) {
if (err instanceof FidacyError) {
console.error(err.type, err.status, err.rejection_reasons);
} else {
throw err;
}
}Options
new Fidacy({
apiKey, // required, sent as a Bearer token
baseUrl, // default 'https://api.fidacy.com'
timeoutMs, // per-request, default 10000
maxRetries, // idempotent calls only, default 2
fetch, // inject a fetch implementation
});Retries apply only when you pass an idempotencyKey to assess, so a retried call can never double-charge or duplicate.
Consequential actions
assess produces a signed risk verdict. actionMandates is the stricter path
for an action that changes the world: it creates bounded authority, then issues
a short-lived grant for one exact request. The executor keeps the business data
and credential; Fidacy receives only the action, resource, and a SHA-256 hash of
the local context.
import { Fidacy, hashActionContext } from '@fidacy/sdk';
const fidacy = new Fidacy({ apiKey: process.env.FIDACY_API_KEY! });
const mandate = await fidacy.actionMandates.create({
subject: 'support-refund-agent',
version: 'fidacy.action-mandate.v1',
allow: {
actions: ['stripe.refund.create'],
resources: ['stripe:payment_intent:pi_123'],
maxActions: 1,
maxDelegationDepth: 0,
},
window: {
notBefore: new Date().toISOString(),
notAfter: new Date(Date.now() + 15 * 60_000).toISOString(),
},
});
const localRefund = { paymentIntentId: 'pi_123', amount: 1234 };
const contextHash = await hashActionContext(localRefund);
const result = await fidacy.actionMandates.decide(mandate.id, {
action: 'stripe.refund.create',
resource: 'stripe:payment_intent:pi_123',
contextHash,
});
if (!result.grant) throw new Error(`Refund refused: ${result.violated_rule}`);
// Give result.grant to a Fidacy-enforcing executor. It must verify the grant,
// redeem it once, then and only then call Stripe.Both ALLOW and DENY return a signed receipt. Operators can list decisions through
fidacy.actionDecisions.list() and export independently verifiable evidence with
fidacy.actionDecisions.incidentPack(decisionId).
License
Apache-2.0. Part of fidacy-open.
